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 }