3 namespace Drupal\locale;
5 use Drupal\Core\Database\Connection;
8 * Defines a class to store localized strings in the database.
10 class StringDatabaseStorage implements StringStorageInterface {
13 * The database connection.
15 * @var \Drupal\Core\Database\Connection
17 protected $connection;
20 * Additional database connection options to use in queries.
24 protected $options = [];
27 * Constructs a new StringDatabaseStorage class.
29 * @param \Drupal\Core\Database\Connection $connection
30 * A Database connection to use for reading and writing configuration data.
31 * @param array $options
32 * (optional) Any additional database connection options to use in queries.
34 public function __construct(Connection $connection, array $options = []) {
35 $this->connection = $connection;
36 $this->options = $options;
42 public function getStrings(array $conditions = [], array $options = []) {
43 return $this->dbStringLoad($conditions, $options, 'Drupal\locale\SourceString');
49 public function getTranslations(array $conditions = [], array $options = []) {
50 return $this->dbStringLoad($conditions, ['translation' => TRUE] + $options, 'Drupal\locale\TranslationString');
56 public function findString(array $conditions) {
57 $values = $this->dbStringSelect($conditions)
61 if (!empty($values)) {
62 $string = new SourceString($values);
63 $string->setStorage($this);
71 public function findTranslation(array $conditions) {
72 $values = $this->dbStringSelect($conditions, ['translation' => TRUE])
76 if (!empty($values)) {
77 $string = new TranslationString($values);
78 $this->checkVersion($string, \Drupal::VERSION);
79 $string->setStorage($this);
87 public function getLocations(array $conditions = []) {
88 $query = $this->connection->select('locales_location', 'l', $this->options)
90 foreach ($conditions as $field => $value) {
91 // Cast scalars to array so we can consistently use an IN condition.
92 $query->condition('l.' . $field, (array) $value, 'IN');
94 return $query->execute()->fetchAll();
100 public function countStrings() {
101 return $this->dbExecute("SELECT COUNT(*) FROM {locales_source}")->fetchField();
107 public function countTranslations() {
108 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();
114 public function save($string) {
115 if ($string->isNew()) {
116 $result = $this->dbStringInsert($string);
117 if ($string->isSource() && $result) {
118 // Only for source strings, we set the locale identifier.
119 $string->setId($result);
121 $string->setStorage($this);
124 $this->dbStringUpdate($string);
126 // Update locations if they come with the string.
127 $this->updateLocation($string);
132 * Update locations for string.
134 * @param \Drupal\locale\StringInterface $string
137 protected function updateLocation($string) {
138 if ($locations = $string->getLocations(TRUE)) {
140 foreach ($locations as $type => $location) {
141 foreach ($location as $name => $lid) {
142 // Make sure that the name isn't longer than 255 characters.
143 $name = substr($name, 0, 255);
145 $this->dbDelete('locales_location', ['sid' => $string->getId(), 'type' => $type, 'name' => $name])
148 elseif ($lid === TRUE) {
149 // This is a new location to add, take care not to duplicate.
150 $this->connection->merge('locales_location', $this->options)
151 ->keys(['sid' => $string->getId(), 'type' => $type, 'name' => $name])
152 ->fields(['version' => \Drupal::VERSION])
156 // Loaded locations have 'lid' integer value, nor FALSE, nor TRUE.
160 // As we've set a new location, check string version too.
161 $this->checkVersion($string, \Drupal::VERSION);
167 * Checks whether the string version matches a given version, fix it if not.
169 * @param \Drupal\locale\StringInterface $string
171 * @param string $version
172 * Drupal version to check against.
174 protected function checkVersion($string, $version) {
175 if ($string->getId() && $string->getVersion() != $version) {
176 $string->setVersion($version);
177 $this->connection->update('locales_source', $this->options)
178 ->condition('lid', $string->getId())
179 ->fields(['version' => $version])
187 public function delete($string) {
188 if ($keys = $this->dbStringKeys($string)) {
189 $this->dbDelete('locales_target', $keys)->execute();
190 if ($string->isSource()) {
191 $this->dbDelete('locales_source', $keys)->execute();
192 $this->dbDelete('locales_location', $keys)->execute();
193 $string->setId(NULL);
197 throw new StringStorageException('The string cannot be deleted because it lacks some key fields: ' . $string->getString());
205 public function deleteStrings($conditions) {
206 $lids = $this->dbStringSelect($conditions, ['fields' => ['lid']])->execute()->fetchCol();
208 $this->dbDelete('locales_target', ['lid' => $lids])->execute();
209 $this->dbDelete('locales_source', ['lid' => $lids])->execute();
210 $this->dbDelete('locales_location', ['sid' => $lids])->execute();
217 public function deleteTranslations($conditions) {
218 $this->dbDelete('locales_target', $conditions)->execute();
224 public function createString($values = []) {
225 return new SourceString($values + ['storage' => $this]);
231 public function createTranslation($values = []) {
232 return new TranslationString($values + [
239 * Gets table alias for field.
241 * @param string $field
242 * One of the field names of the locales_source, locates_location,
243 * locales_target tables to find the table alias for.
246 * One of the following values:
247 * - 's' for "source", "context", "version" (locales_source table fields).
248 * - 'l' for "type", "name" (locales_location table fields)
249 * - 't' for "language", "translation", "customized" (locales_target
252 protected function dbFieldTable($field) {
253 if (in_array($field, ['language', 'translation', 'customized'])) {
256 elseif (in_array($field, ['type', 'name'])) {
265 * Gets table name for storing string object.
267 * @param \Drupal\locale\StringInterface $string
273 protected function dbStringTable($string) {
274 if ($string->isSource()) {
275 return 'locales_source';
277 elseif ($string->isTranslation()) {
278 return 'locales_target';
283 * Gets keys values that are in a database table.
285 * @param \Drupal\locale\StringInterface $string
289 * Array with key fields if the string has all keys, or empty array if not.
291 protected function dbStringKeys($string) {
292 if ($string->isSource()) {
295 elseif ($string->isTranslation()) {
296 $keys = ['lid', 'language'];
298 if (!empty($keys) && ($values = $string->getValues($keys)) && count($keys) == count($values)) {
307 * Loads multiple string objects.
309 * @param array $conditions
310 * Any of the conditions used by dbStringSelect().
311 * @param array $options
312 * Any of the options used by dbStringSelect().
313 * @param string $class
314 * Class name to use for fetching returned objects.
316 * @return \Drupal\locale\StringInterface[]
317 * Array of objects of the class requested.
319 protected function dbStringLoad(array $conditions, array $options, $class) {
321 $result = $this->dbStringSelect($conditions, $options)->execute();
322 foreach ($result as $item) {
323 /** @var \Drupal\locale\StringInterface $string */
324 $string = new $class($item);
325 $string->setStorage($this);
326 $strings[] = $string;
332 * Builds a SELECT query with multiple conditions and fields.
334 * The query uses both 'locales_source' and 'locales_target' tables.
335 * Note that by default, as we are selecting both translated and untranslated
336 * strings target field's conditions will be modified to match NULL rows too.
338 * @param array $conditions
339 * An associative array with field => value conditions that may include
340 * NULL values. If a language condition is included it will be used for
341 * joining the 'locales_target' table.
342 * @param array $options
343 * An associative array of additional options. It may contain any of the
344 * options used by Drupal\locale\StringStorageInterface::getStrings() and
345 * these additional ones:
346 * - 'translation', Whether to include translation fields too. Defaults to
349 * @return \Drupal\Core\Database\Query\Select
350 * Query object with all the tables, fields and conditions.
352 protected function dbStringSelect(array $conditions, array $options = []) {
353 // Start building the query with source table and check whether we need to
354 // join the target table too.
355 $query = $this->connection->select('locales_source', 's', $this->options)
358 // Figure out how to join and translate some options into conditions.
359 if (isset($conditions['translated'])) {
360 // This is a meta-condition we need to translate into simple ones.
361 if ($conditions['translated']) {
362 // Select only translated strings.
366 // Select only untranslated strings.
368 $conditions['translation'] = NULL;
370 unset($conditions['translated']);
373 $join = !empty($options['translation']) ? 'leftJoin' : FALSE;
377 if (isset($conditions['language'])) {
378 // If we've got a language condition, we use it for the join.
379 $query->$join('locales_target', 't', "t.lid = s.lid AND t.language = :langcode", [
380 ':langcode' => $conditions['language'],
382 unset($conditions['language']);
385 // Since we don't have a language, join with locale id only.
386 $query->$join('locales_target', 't', "t.lid = s.lid");
388 if (!empty($options['translation'])) {
389 // We cannot just add all fields because 'lid' may get null values.
390 $query->fields('t', ['language', 'translation', 'customized']);
394 // If we have conditions for location's type or name, then we need the
395 // location table, for which we add a subquery. We cast any scalar value to
396 // array so we can consistently use IN conditions.
397 if (isset($conditions['type']) || isset($conditions['name'])) {
398 $subquery = $this->connection->select('locales_location', 'l', $this->options)
399 ->fields('l', ['sid']);
400 foreach (['type', 'name'] as $field) {
401 if (isset($conditions[$field])) {
402 $subquery->condition('l.' . $field, (array) $conditions[$field], 'IN');
403 unset($conditions[$field]);
406 $query->condition('s.lid', $subquery, 'IN');
409 // Add conditions for both tables.
410 foreach ($conditions as $field => $value) {
411 $table_alias = $this->dbFieldTable($field);
412 $field_alias = $table_alias . '.' . $field;
413 if (is_null($value)) {
414 $query->isNull($field_alias);
416 elseif ($table_alias == 't' && $join === 'leftJoin') {
417 // Conditions for target fields when doing an outer join only make
418 // sense if we add also OR field IS NULL.
419 $query->condition(db_or()
420 ->condition($field_alias, (array) $value, 'IN')
421 ->isNull($field_alias)
425 $query->condition($field_alias, (array) $value, 'IN');
429 // Process other options, string filter, query limit, etc.
430 if (!empty($options['filters'])) {
431 if (count($options['filters']) > 1) {
433 $query->condition($filter);
436 // If we have a single filter, just add it to the query.
439 foreach ($options['filters'] as $field => $string) {
440 $filter->condition($this->dbFieldTable($field) . '.' . $field, '%' . db_like($string) . '%', 'LIKE');
444 if (!empty($options['pager limit'])) {
445 $query = $query->extend('Drupal\Core\Database\Query\PagerSelectExtender')->limit($options['pager limit']);
452 * Creates a database record for a string object.
454 * @param \Drupal\locale\StringInterface $string
458 * If the operation failed, returns FALSE.
459 * If it succeeded returns the last insert ID of the query, if one exists.
461 * @throws \Drupal\locale\StringStorageException
462 * If the string is not suitable for this storage, an exception is thrown.
464 protected function dbStringInsert($string) {
465 if ($string->isSource()) {
466 $string->setValues(['context' => '', 'version' => 'none'], FALSE);
467 $fields = $string->getValues(['source', 'context', 'version']);
469 elseif ($string->isTranslation()) {
470 $string->setValues(['customized' => 0], FALSE);
471 $fields = $string->getValues(['lid', 'language', 'translation', 'customized']);
473 if (!empty($fields)) {
474 return $this->connection->insert($this->dbStringTable($string), $this->options)
479 throw new StringStorageException('The string cannot be saved: ' . $string->getString());
484 * Updates string object in the database.
486 * @param \Drupal\locale\StringInterface $string
490 * If the record update failed, returns FALSE. If it succeeded, returns
491 * SAVED_NEW or SAVED_UPDATED.
493 * @throws \Drupal\locale\StringStorageException
494 * If the string is not suitable for this storage, an exception is thrown.
496 protected function dbStringUpdate($string) {
497 if ($string->isSource()) {
498 $values = $string->getValues(['source', 'context', 'version']);
500 elseif ($string->isTranslation()) {
501 $values = $string->getValues(['translation', 'customized']);
503 if (!empty($values) && $keys = $this->dbStringKeys($string)) {
504 return $this->connection->merge($this->dbStringTable($string), $this->options)
510 throw new StringStorageException('The string cannot be updated: ' . $string->getString());
515 * Creates delete query.
517 * @param string $table
520 * Array with object keys indexed by field name.
522 * @return \Drupal\Core\Database\Query\Delete
523 * Returns a new Delete object for the injected database connection.
525 protected function dbDelete($table, $keys) {
526 $query = $this->connection->delete($table, $this->options);
527 foreach ($keys as $field => $value) {
528 $query->condition($field, $value);
534 * Executes an arbitrary SELECT query string with the injected options.
536 protected function dbExecute($query, array $args = []) {
537 return $this->connection->query($query, $args, $this->options);