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