StringDatabaseStorage.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. <?php
  2. /**
  3. * @file
  4. * Definition of StringDatabaseStorage.
  5. */
  6. /**
  7. * Defines the locale string class.
  8. *
  9. * This is the base class for SourceString and TranslationString.
  10. */
  11. class StringDatabaseStorage implements StringStorageInterface {
  12. /**
  13. * Additional database connection options to use in queries.
  14. *
  15. * @var array
  16. */
  17. protected $options = array();
  18. /**
  19. * Constructs a new StringStorage controller.
  20. *
  21. * @param array $options
  22. * (optional) Any additional database connection options to use in queries.
  23. */
  24. public function __construct(array $options = array()) {
  25. $this->options = $options;
  26. }
  27. /**
  28. * Implements StringStorageInterface::getStrings().
  29. */
  30. public function getStrings(array $conditions = array(), array $options = array()) {
  31. return $this->dbStringLoad($conditions, $options, 'SourceString');
  32. }
  33. /**
  34. * Implements StringStorageInterface::getTranslations().
  35. */
  36. public function getTranslations(array $conditions = array(), array $options = array()) {
  37. return $this->dbStringLoad($conditions, array('translation' => TRUE) + $options, 'TranslationString');
  38. }
  39. /**
  40. * Implements StringStorageInterface::findString().
  41. */
  42. public function findString(array $conditions) {
  43. $values = $this->dbStringSelect($conditions)
  44. ->execute()
  45. ->fetchAssoc();
  46. if (!empty($values)) {
  47. $string = new SourceString($values);
  48. $string->setStorage($this);
  49. return $string;
  50. }
  51. }
  52. /**
  53. * Implements StringStorageInterface::findTranslation().
  54. */
  55. public function findTranslation(array $conditions) {
  56. $values = $this->dbStringSelect($conditions, array('translation' => TRUE))
  57. ->execute()
  58. ->fetchAssoc();
  59. if (!empty($values)) {
  60. $string = new TranslationString($values);
  61. $this->checkVersion($string, VERSION);
  62. $string->setStorage($this);
  63. return $string;
  64. }
  65. }
  66. /**
  67. * Implements StringStorageInterface::countStrings().
  68. */
  69. public function countStrings() {
  70. return $this->dbExecute("SELECT COUNT(*) FROM {locales_source}")->fetchField();
  71. }
  72. /**
  73. * Implements StringStorageInterface::countTranslations().
  74. */
  75. public function countTranslations() {
  76. return $this->dbExecute("SELECT t.language, COUNT(*) AS translated FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid GROUP BY t.language")->fetchAllKeyed();
  77. }
  78. /**
  79. * Implements StringStorageInterface::save().
  80. */
  81. public function save($string) {
  82. if ($string->isNew()) {
  83. $result = $this->dbStringInsert($string);
  84. if ($string->isSource() && $result) {
  85. // Only for source strings, we set the locale identifier.
  86. $string->setId($result);
  87. }
  88. $string->setStorage($this);
  89. }
  90. else {
  91. $this->dbStringUpdate($string);
  92. }
  93. return $this;
  94. }
  95. /**
  96. * Checks whether the string version matches a given version, fix it if not.
  97. *
  98. * @param StringInterface $string
  99. * The string object.
  100. * @param string $version
  101. * Drupal version to check against.
  102. */
  103. protected function checkVersion($string, $version) {
  104. if ($string->getId() && $string->getVersion() != $version) {
  105. $string->setVersion($version);
  106. db_update('locales_source', $this->options)
  107. ->condition('lid', $string->getId())
  108. ->fields(array('version' => $version))
  109. ->execute();
  110. }
  111. }
  112. /**
  113. * Implements StringStorageInterface::delete().
  114. */
  115. public function delete($string) {
  116. if ($keys = $this->dbStringKeys($string)) {
  117. $this->dbDelete('locales_target', $keys)->execute();
  118. if ($string->isSource()) {
  119. $this->dbDelete('locales_source', $keys)->execute();
  120. $this->dbDelete('locales_location', $keys)->execute();
  121. $string->setId(NULL);
  122. }
  123. }
  124. else {
  125. throw new StringStorageException(format_string('The string cannot be deleted because it lacks some key fields: @string', array(
  126. '@string' => $string->getString()
  127. )));
  128. }
  129. return $this;
  130. }
  131. /**
  132. * Implements StringStorageInterface::deleteLanguage().
  133. */
  134. public function deleteStrings($conditions) {
  135. $lids = $this->dbStringSelect($conditions, array('fields' => array('lid')))->execute()->fetchCol();
  136. if ($lids) {
  137. $this->dbDelete('locales_target', array('lid' => $lids))->execute();
  138. $this->dbDelete('locales_source', array('lid' => $lids))->execute();
  139. $this->dbDelete('locales_location', array('sid' => $lids))->execute();
  140. }
  141. }
  142. /**
  143. * Implements StringStorageInterface::deleteLanguage().
  144. */
  145. public function deleteTranslations($conditions) {
  146. $this->dbDelete('locales_target', $conditions)->execute();
  147. }
  148. /**
  149. * Implements StringStorageInterface::createString().
  150. */
  151. public function createString($values = array()) {
  152. return new SourceString($values + array('storage' => $this));
  153. }
  154. /**
  155. * Implements StringStorageInterface::createTranslation().
  156. */
  157. public function createTranslation($values = array()) {
  158. return new TranslationString($values + array(
  159. 'storage' => $this,
  160. 'is_new' => TRUE
  161. ));
  162. }
  163. /**
  164. * Gets table alias for field.
  165. *
  166. * @param string $field
  167. * Field name to find the table alias for.
  168. *
  169. * @return string
  170. * Either 's', 't' or 'l' depending on whether the field belongs to source,
  171. * target or location table table.
  172. */
  173. protected function dbFieldTable($field) {
  174. if (in_array($field, array('language', 'translation', 'customized'))) {
  175. return 't';
  176. }
  177. elseif (in_array($field, array('type', 'name'))) {
  178. return 'l';
  179. }
  180. else {
  181. return 's';
  182. }
  183. }
  184. /**
  185. * Gets table name for storing string object.
  186. *
  187. * @param StringInterface $string
  188. * The string object.
  189. *
  190. * @return string
  191. * The table name.
  192. */
  193. protected function dbStringTable($string) {
  194. if ($string->isSource()) {
  195. return 'locales_source';
  196. }
  197. elseif ($string->isTranslation()) {
  198. return 'locales_target';
  199. }
  200. }
  201. /**
  202. * Gets keys values that are in a database table.
  203. *
  204. * @param StringInterface $string
  205. * The string object.
  206. *
  207. * @return array
  208. * Array with key fields if the string has all keys, or empty array if not.
  209. */
  210. protected function dbStringKeys($string) {
  211. if ($string->isSource()) {
  212. $keys = array('lid');
  213. }
  214. elseif ($string->isTranslation()) {
  215. $keys = array('lid', 'language');
  216. }
  217. if (!empty($keys) && ($values = $string->getValues($keys)) && count($keys) == count($values)) {
  218. return $values;
  219. }
  220. else {
  221. return array();
  222. }
  223. }
  224. /**
  225. * Loads multiple string objects.
  226. *
  227. * @param array $conditions
  228. * Any of the conditions used by dbStringSelect().
  229. * @param array $options
  230. * Any of the options used by dbStringSelect().
  231. * @param string $class
  232. * Class name to use for fetching returned objects.
  233. *
  234. * @return array
  235. * Array of objects of the class requested.
  236. */
  237. protected function dbStringLoad(array $conditions, array $options, $class) {
  238. $strings = array();
  239. $result = $this->dbStringSelect($conditions, $options)->execute();
  240. foreach ($result as $item) {
  241. $string = new $class($item);
  242. $string->setStorage($this);
  243. $strings[] = $string;
  244. }
  245. return $strings;
  246. }
  247. /**
  248. * Builds a SELECT query with multiple conditions and fields.
  249. *
  250. * The query uses both 'locales_source' and 'locales_target' tables.
  251. * Note that by default, as we are selecting both translated and untranslated
  252. * strings target field's conditions will be modified to match NULL rows too.
  253. *
  254. * @param array $conditions
  255. * An associative array with field => value conditions that may include
  256. * NULL values. If a language condition is included it will be used for
  257. * joining the 'locales_target' table.
  258. * @param array $options
  259. * An associative array of additional options. It may contain any of the
  260. * options used by StringStorageInterface::getStrings() and these additional
  261. * ones:
  262. * - 'translation', Whether to include translation fields too. Defaults to
  263. * FALSE.
  264. * @return SelectQuery
  265. * Query object with all the tables, fields and conditions.
  266. */
  267. protected function dbStringSelect(array $conditions, array $options = array()) {
  268. // Change field 'customized' into 'l10n_status'. This enables the Drupal 8
  269. // backported code to work with the Drupal 7 style database tables.
  270. if (isset($conditions['customized'])) {
  271. $conditions['l10n_status'] = $conditions['customized'];
  272. unset($conditions['customized']);
  273. }
  274. if (isset($options['customized'])) {
  275. $options['l10n_status'] = $options['customized'];
  276. unset($options['customized']);
  277. }
  278. // Start building the query with source table and check whether we need to
  279. // join the target table too.
  280. $query = db_select('locales_source', 's', $this->options)
  281. ->fields('s');
  282. // Figure out how to join and translate some options into conditions.
  283. if (isset($conditions['translated'])) {
  284. // This is a meta-condition we need to translate into simple ones.
  285. if ($conditions['translated']) {
  286. // Select only translated strings.
  287. $join = 'innerJoin';
  288. }
  289. else {
  290. // Select only untranslated strings.
  291. $join = 'leftJoin';
  292. $conditions['translation'] = NULL;
  293. }
  294. unset($conditions['translated']);
  295. }
  296. else {
  297. $join = !empty($options['translation']) ? 'leftJoin' : FALSE;
  298. }
  299. if ($join) {
  300. if (isset($conditions['language'])) {
  301. // If we've got a language condition, we use it for the join.
  302. $query->$join('locales_target', 't', "t.lid = s.lid AND t.language = :langcode", array(
  303. ':langcode' => $conditions['language']
  304. ));
  305. unset($conditions['language']);
  306. }
  307. else {
  308. // Since we don't have a language, join with locale id only.
  309. $query->$join('locales_target', 't', "t.lid = s.lid");
  310. }
  311. if (!empty($options['translation'])) {
  312. // We cannot just add all fields because 'lid' may get null values.
  313. $query->addField('t', 'language');
  314. $query->addField('t', 'translation');
  315. $query->addField('t', 'l10n_status', 'customized');
  316. }
  317. }
  318. // If we have conditions for location's type or name, then we need the
  319. // location table, for which we add a subquery.
  320. if (isset($conditions['type']) || isset($conditions['name'])) {
  321. $subquery = db_select('locales_location', 'l', $this->options)
  322. ->fields('l', array('sid'));
  323. foreach (array('type', 'name') as $field) {
  324. if (isset($conditions[$field])) {
  325. $subquery->condition('l.' . $field, $conditions[$field]);
  326. unset($conditions[$field]);
  327. }
  328. }
  329. $query->condition('s.lid', $subquery, 'IN');
  330. }
  331. // Add conditions for both tables.
  332. foreach ($conditions as $field => $value) {
  333. $table_alias = $this->dbFieldTable($field);
  334. $field_alias = $table_alias . '.' . $field;
  335. if (is_null($value)) {
  336. $query->isNull($field_alias);
  337. }
  338. elseif ($table_alias == 't' && $join === 'leftJoin') {
  339. // Conditions for target fields when doing an outer join only make
  340. // sense if we add also OR field IS NULL.
  341. $query->condition(db_or()
  342. ->condition($field_alias, $value)
  343. ->isNull($field_alias)
  344. );
  345. }
  346. else {
  347. $query->condition($field_alias, $value);
  348. }
  349. }
  350. // Process other options, string filter, query limit, etc...
  351. if (!empty($options['filters'])) {
  352. if (count($options['filters']) > 1) {
  353. $filter = db_or();
  354. $query->condition($filter);
  355. }
  356. else {
  357. // If we have a single filter, just add it to the query.
  358. $filter = $query;
  359. }
  360. foreach ($options['filters'] as $field => $string) {
  361. $filter->condition($this->dbFieldTable($field) . '.' . $field, '%' . db_like($string) . '%', 'LIKE');
  362. }
  363. }
  364. if (!empty($options['pager limit'])) {
  365. $query = $query->extend('PagerDefault')->limit($options['pager limit']);
  366. }
  367. return $query;
  368. }
  369. /**
  370. * Createds a database record for a string object.
  371. *
  372. * @param StringInterface $string
  373. * The string object.
  374. *
  375. * @return bool|int
  376. * If the operation failed, returns FALSE.
  377. * If it succeeded returns the last insert ID of the query, if one exists.
  378. *
  379. * @throws StringStorageException
  380. * If the string is not suitable for this storage, an exception ithrown.
  381. */
  382. protected function dbStringInsert($string) {
  383. if ($string->isSource()) {
  384. $string->setValues(array('context' => '', 'version' => 'none'), FALSE);
  385. $fields = $string->getValues(array('source', 'context', 'version'));
  386. }
  387. elseif ($string->isTranslation()) {
  388. $string->setValues(array('customized' => 0), FALSE);
  389. $fields = $string->getValues(array('lid', 'language', 'translation', 'customized'));
  390. }
  391. if (!empty($fields)) {
  392. // Change field 'customized' into 'l10n_status'. This enables the Drupal 8
  393. // backported code to work with the Drupal 7 style database tables.
  394. if (isset($fields['customized'])) {
  395. $fields['l10n_status'] = $fields['customized'];
  396. unset($fields['customized']);
  397. }
  398. return db_insert($this->dbStringTable($string), $this->options)
  399. ->fields($fields)
  400. ->execute();
  401. }
  402. else {
  403. throw new StringStorageException(format_string('The string cannot be saved: @string', array(
  404. '@string' => $string->getString()
  405. )));
  406. }
  407. }
  408. /**
  409. * Updates string object in the database.
  410. *
  411. * @param StringInterface $string
  412. * The string object.
  413. *
  414. * @return bool|int
  415. * If the record update failed, returns FALSE. If it succeeded, returns
  416. * SAVED_NEW or SAVED_UPDATED.
  417. *
  418. * @throws StringStorageException
  419. * If the string is not suitable for this storage, an exception is thrown.
  420. */
  421. protected function dbStringUpdate($string) {
  422. if ($string->isSource()) {
  423. $values = $string->getValues(array('source', 'context', 'version'));
  424. }
  425. elseif ($string->isTranslation()) {
  426. $values = $string->getValues(array('translation', 'customized'));
  427. }
  428. if (!empty($values) && $keys = $this->dbStringKeys($string)) {
  429. // Change field 'customized' into 'l10n_status'. This enables the Drupal 8
  430. // backported code to work with the Drupal 7 style database tables.
  431. if (isset($keys['customized'])) {
  432. $keys['l10n_status'] = $keys['customized'];
  433. unset($keys['customized']);
  434. }
  435. if (isset($values['customized'])) {
  436. $values['l10n_status'] = $values['customized'];
  437. unset($values['customized']);
  438. }
  439. return db_merge($this->dbStringTable($string), $this->options)
  440. ->key($keys)
  441. ->fields($values)
  442. ->execute();
  443. }
  444. else {
  445. throw new StringStorageException(format_string('The string cannot be updated: @string', array(
  446. '@string' => $string->getString()
  447. )));
  448. }
  449. }
  450. /**
  451. * Creates delete query.
  452. *
  453. * @param string $table
  454. * The table name.
  455. * @param array $keys
  456. * Array with object keys indexed by field name.
  457. *
  458. * @return DeleteQuery
  459. * Returns a new DeleteQuery object for the active database.
  460. */
  461. protected function dbDelete($table, $keys) {
  462. $query = db_delete($table, $this->options);
  463. // Change field 'customized' into 'l10n_status'. This enables the Drupal 8
  464. // backported code to work with the Drupal 7 style database tables.
  465. if (isset($keys['customized'])) {
  466. $keys['l10n_status'] = $keys['customized'];
  467. unset($keys['customized']);
  468. }
  469. foreach ($keys as $field => $value) {
  470. $query->condition($field, $value);
  471. }
  472. return $query;
  473. }
  474. /**
  475. * Executes an arbitrary SELECT query string.
  476. */
  477. protected function dbExecute($query, array $args = array()) {
  478. return db_query($query, $args, $this->options);
  479. }
  480. }