StringDatabaseStorage.php

Same filename in other branches
  1. 9 core/modules/locale/src/StringDatabaseStorage.php
  2. 8.9.x core/modules/locale/src/StringDatabaseStorage.php
  3. 11.x core/modules/locale/src/StringDatabaseStorage.php

Namespace

Drupal\locale

File

core/modules/locale/src/StringDatabaseStorage.php

View source
<?php

namespace Drupal\locale;

use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\PagerSelectExtender;

/**
 * Defines a class to store localized strings in the database.
 */
class StringDatabaseStorage implements StringStorageInterface {
    
    /**
     * The database connection.
     *
     * @var \Drupal\Core\Database\Connection
     */
    protected $connection;
    
    /**
     * Additional database connection options to use in queries.
     *
     * @var array
     */
    protected $options = [];
    
    /**
     * Constructs a new StringDatabaseStorage class.
     *
     * @param \Drupal\Core\Database\Connection $connection
     *   A Database connection to use for reading and writing configuration data.
     * @param array $options
     *   (optional) Any additional database connection options to use in queries.
     */
    public function __construct(Connection $connection, array $options = []) {
        $this->connection = $connection;
        $this->options = $options;
    }
    
    /**
     * {@inheritdoc}
     */
    public function getStrings(array $conditions = [], array $options = []) {
        return $this->dbStringLoad($conditions, $options, 'Drupal\\locale\\SourceString');
    }
    
    /**
     * {@inheritdoc}
     */
    public function getTranslations(array $conditions = [], array $options = []) {
        return $this->dbStringLoad($conditions, [
            'translation' => TRUE,
        ] + $options, 'Drupal\\locale\\TranslationString');
    }
    
    /**
     * {@inheritdoc}
     */
    public function findString(array $conditions) {
        $values = $this->dbStringSelect($conditions)
            ->execute()
            ->fetchAssoc();
        if (!empty($values)) {
            $string = new SourceString($values);
            $string->setStorage($this);
            return $string;
        }
    }
    
    /**
     * {@inheritdoc}
     */
    public function findTranslation(array $conditions) {
        $values = $this->dbStringSelect($conditions, [
            'translation' => TRUE,
        ])
            ->execute()
            ->fetchAssoc();
        if (!empty($values)) {
            $string = new TranslationString($values);
            $this->checkVersion($string, \Drupal::VERSION);
            $string->setStorage($this);
            return $string;
        }
    }
    
    /**
     * {@inheritdoc}
     */
    public function getLocations(array $conditions = []) {
        $query = $this->connection
            ->select('locales_location', 'l', $this->options)
            ->fields('l');
        foreach ($conditions as $field => $value) {
            // Cast scalars to array so we can consistently use an IN condition.
            $query->condition('l.' . $field, (array) $value, 'IN');
        }
        return $query->execute()
            ->fetchAll();
    }
    
    /**
     * {@inheritdoc}
     */
    public function countStrings() {
        return $this->dbExecute("SELECT COUNT(*) FROM {locales_source}")
            ->fetchField();
    }
    
    /**
     * {@inheritdoc}
     */
    public function countTranslations() {
        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();
    }
    
    /**
     * {@inheritdoc}
     */
    public function save($string) {
        if ($string->isNew()) {
            $result = $this->dbStringInsert($string);
            if ($string->isSource() && $result) {
                // Only for source strings, we set the locale identifier.
                $string->setId($result);
            }
            $string->setStorage($this);
        }
        else {
            $this->dbStringUpdate($string);
        }
        // Update locations if they come with the string.
        $this->updateLocation($string);
        return $this;
    }
    
    /**
     * Update locations for string.
     *
     * @param \Drupal\locale\StringInterface $string
     *   The string object.
     */
    protected function updateLocation($string) {
        if ($locations = $string->getLocations(TRUE)) {
            $created = FALSE;
            foreach ($locations as $type => $location) {
                foreach ($location as $name => $lid) {
                    // Make sure that the name isn't longer than 255 characters.
                    $name = substr($name, 0, 255);
                    if (!$lid) {
                        $this->dbDelete('locales_location', [
                            'sid' => $string->getId(),
                            'type' => $type,
                            'name' => $name,
                        ])
                            ->execute();
                    }
                    elseif ($lid === TRUE) {
                        // This is a new location to add, take care not to duplicate.
                        $this->connection
                            ->merge('locales_location', $this->options)
                            ->keys([
                            'sid' => $string->getId(),
                            'type' => $type,
                            'name' => $name,
                        ])
                            ->fields([
                            'version' => \Drupal::VERSION,
                        ])
                            ->execute();
                        $created = TRUE;
                    }
                    // Loaded locations have 'lid' integer value, nor FALSE, nor TRUE.
                }
            }
            if ($created) {
                // As we've set a new location, check string version too.
                $this->checkVersion($string, \Drupal::VERSION);
            }
        }
    }
    
    /**
     * Checks whether the string version matches a given version, fix it if not.
     *
     * @param \Drupal\locale\StringInterface $string
     *   The string object.
     * @param string $version
     *   Drupal version to check against.
     */
    protected function checkVersion($string, $version) {
        if ($string->getId() && $string->getVersion() != $version) {
            $string->setVersion($version);
            $this->connection
                ->update('locales_source', $this->options)
                ->condition('lid', $string->getId())
                ->fields([
                'version' => $version,
            ])
                ->execute();
        }
    }
    
    /**
     * {@inheritdoc}
     */
    public function delete($string) {
        if ($keys = $this->dbStringKeys($string)) {
            $this->dbDelete('locales_target', $keys)
                ->execute();
            if ($string->isSource()) {
                $this->dbDelete('locales_source', $keys)
                    ->execute();
                $this->dbDelete('locales_location', $keys)
                    ->execute();
                $string->setId(NULL);
            }
        }
        else {
            throw new StringStorageException('The string cannot be deleted because it lacks some key fields: ' . $string->getString());
        }
        return $this;
    }
    
    /**
     * {@inheritdoc}
     */
    public function deleteStrings($conditions) {
        $lids = $this->dbStringSelect($conditions, [
            'fields' => [
                'lid',
            ],
        ])
            ->execute()
            ->fetchCol();
        if ($lids) {
            $this->dbDelete('locales_target', [
                'lid' => $lids,
            ])
                ->execute();
            $this->dbDelete('locales_source', [
                'lid' => $lids,
            ])
                ->execute();
            $this->dbDelete('locales_location', [
                'sid' => $lids,
            ])
                ->execute();
        }
    }
    
    /**
     * {@inheritdoc}
     */
    public function deleteTranslations($conditions) {
        $this->dbDelete('locales_target', $conditions)
            ->execute();
    }
    
    /**
     * {@inheritdoc}
     */
    public function createString($values = []) {
        return new SourceString($values + [
            'storage' => $this,
        ]);
    }
    
    /**
     * {@inheritdoc}
     */
    public function createTranslation($values = []) {
        return new TranslationString($values + [
            'storage' => $this,
            'is_new' => TRUE,
        ]);
    }
    
    /**
     * Gets table alias for field.
     *
     * @param string $field
     *   One of the field names of the locales_source, locates_location,
     *   locales_target tables to find the table alias for.
     *
     * @return string
     *   One of the following values:
     *   - 's' for "source", "context", "version" (locales_source table fields).
     *   - 'l' for "type", "name" (locales_location table fields)
     *   - 't' for "language", "translation", "customized" (locales_target
     *     table fields)
     */
    protected function dbFieldTable($field) {
        if (in_array($field, [
            'language',
            'translation',
            'customized',
        ])) {
            return 't';
        }
        elseif (in_array($field, [
            'type',
            'name',
        ])) {
            return 'l';
        }
        else {
            return 's';
        }
    }
    
    /**
     * Gets table name for storing string object.
     *
     * @param \Drupal\locale\StringInterface $string
     *   The string object.
     *
     * @return string
     *   The table name.
     */
    protected function dbStringTable($string) {
        if ($string->isSource()) {
            return 'locales_source';
        }
        elseif ($string->isTranslation()) {
            return 'locales_target';
        }
    }
    
    /**
     * Gets keys values that are in a database table.
     *
     * @param \Drupal\locale\StringInterface $string
     *   The string object.
     *
     * @return array
     *   Array with key fields if the string has all keys, or empty array if not.
     */
    protected function dbStringKeys($string) {
        if ($string->isSource()) {
            $keys = [
                'lid',
            ];
        }
        elseif ($string->isTranslation()) {
            $keys = [
                'lid',
                'language',
            ];
        }
        if (!empty($keys) && ($values = $string->getValues($keys)) && count($keys) == count($values)) {
            return $values;
        }
        else {
            return [];
        }
    }
    
    /**
     * Loads multiple string objects.
     *
     * @param array $conditions
     *   Any of the conditions used by dbStringSelect().
     * @param array $options
     *   Any of the options used by dbStringSelect().
     * @param string $class
     *   Class name to use for fetching returned objects.
     *
     * @return \Drupal\locale\StringInterface[]
     *   Array of objects of the class requested.
     */
    protected function dbStringLoad(array $conditions, array $options, $class) {
        $strings = [];
        $result = $this->dbStringSelect($conditions, $options)
            ->execute();
        foreach ($result as $item) {
            
            /** @var \Drupal\locale\StringInterface $string */
            $string = new $class($item);
            $string->setStorage($this);
            $strings[] = $string;
        }
        return $strings;
    }
    
    /**
     * Builds a SELECT query with multiple conditions and fields.
     *
     * The query uses both 'locales_source' and 'locales_target' tables.
     * Note that by default, as we are selecting both translated and untranslated
     * strings target field's conditions will be modified to match NULL rows too.
     *
     * @param array $conditions
     *   An associative array with field => value conditions that may include
     *   NULL values. If a language condition is included it will be used for
     *   joining the 'locales_target' table.
     * @param array $options
     *   An associative array of additional options. It may contain any of the
     *   options used by Drupal\locale\StringStorageInterface::getStrings() and
     *   these additional ones:
     *   - 'translation', Whether to include translation fields too. Defaults to
     *     FALSE.
     *
     * @return \Drupal\Core\Database\Query\Select
     *   Query object with all the tables, fields and conditions.
     */
    protected function dbStringSelect(array $conditions, array $options = []) {
        // Start building the query with source table and check whether we need to
        // join the target table too.
        $query = $this->connection
            ->select('locales_source', 's', $this->options)
            ->fields('s');
        // Figure out how to join and translate some options into conditions.
        if (isset($conditions['translated'])) {
            // This is a meta-condition we need to translate into simple ones.
            if ($conditions['translated']) {
                // Select only translated strings.
                $join = 'innerJoin';
            }
            else {
                // Select only untranslated strings.
                $join = 'leftJoin';
                $conditions['translation'] = NULL;
            }
            unset($conditions['translated']);
        }
        else {
            $join = !empty($options['translation']) ? 'leftJoin' : FALSE;
        }
        if ($join) {
            if (isset($conditions['language'])) {
                // If we've got a language condition, we use it for the join.
                $query->{$join}('locales_target', 't', "t.lid = s.lid AND t.language = :langcode", [
                    ':langcode' => $conditions['language'],
                ]);
                unset($conditions['language']);
            }
            else {
                // Since we don't have a language, join with locale id only.
                $query->{$join}('locales_target', 't', "t.lid = s.lid");
            }
            if (!empty($options['translation'])) {
                // We cannot just add all fields because 'lid' may get null values.
                $query->fields('t', [
                    'language',
                    'translation',
                    'customized',
                ]);
            }
        }
        // If we have conditions for location's type or name, then we need the
        // location table, for which we add a subquery. We cast any scalar value to
        // array so we can consistently use IN conditions.
        if (isset($conditions['type']) || isset($conditions['name'])) {
            $subquery = $this->connection
                ->select('locales_location', 'l', $this->options)
                ->fields('l', [
                'sid',
            ]);
            foreach ([
                'type',
                'name',
            ] as $field) {
                if (isset($conditions[$field])) {
                    $subquery->condition('l.' . $field, (array) $conditions[$field], 'IN');
                    unset($conditions[$field]);
                }
            }
            $query->condition('s.lid', $subquery, 'IN');
        }
        // Add conditions for both tables.
        foreach ($conditions as $field => $value) {
            $table_alias = $this->dbFieldTable($field);
            $field_alias = $table_alias . '.' . $field;
            if (is_null($value)) {
                $query->isNull($field_alias);
            }
            elseif ($table_alias == 't' && $join === 'leftJoin') {
                // Conditions for target fields when doing an outer join only make
                // sense if we add also OR field IS NULL.
                $query->condition($this->connection
                    ->condition('OR')
                    ->condition($field_alias, (array) $value, 'IN')
                    ->isNull($field_alias));
            }
            else {
                $query->condition($field_alias, (array) $value, 'IN');
            }
        }
        // Process other options, string filter, query limit, etc.
        if (!empty($options['filters'])) {
            if (count($options['filters']) > 1) {
                $filter = $this->connection
                    ->condition('OR');
                $query->condition($filter);
            }
            else {
                // If we have a single filter, just add it to the query.
                $filter = $query;
            }
            foreach ($options['filters'] as $field => $string) {
                $filter->condition($this->dbFieldTable($field) . '.' . $field, '%' . $this->connection
                    ->escapeLike($string) . '%', 'LIKE');
            }
        }
        if (!empty($options['pager limit'])) {
            $query = $query->extend(PagerSelectExtender::class)
                ->limit($options['pager limit']);
        }
        return $query;
    }
    
    /**
     * Creates a database record for a string object.
     *
     * @param \Drupal\locale\StringInterface $string
     *   The string object.
     *
     * @return bool|int
     *   If the operation failed, returns FALSE.
     *   If it succeeded returns the last insert ID of the query, if one exists.
     *
     * @throws \Drupal\locale\StringStorageException
     *   If the string is not suitable for this storage, an exception is thrown.
     */
    protected function dbStringInsert($string) {
        if ($string->isSource()) {
            $string->setValues([
                'context' => '',
                'version' => 'none',
            ], FALSE);
            $fields = $string->getValues([
                'source',
                'context',
                'version',
            ]);
        }
        elseif ($string->isTranslation()) {
            $string->setValues([
                'customized' => 0,
            ], FALSE);
            $fields = $string->getValues([
                'lid',
                'language',
                'translation',
                'customized',
            ]);
        }
        if (!empty($fields)) {
            return $this->connection
                ->insert($this->dbStringTable($string), $this->options)
                ->fields($fields)
                ->execute();
        }
        else {
            throw new StringStorageException('The string cannot be saved: ' . $string->getString());
        }
    }
    
    /**
     * Updates string object in the database.
     *
     * @param \Drupal\locale\StringInterface $string
     *   The string object.
     *
     * @return bool|int
     *   If the record update failed, returns FALSE. If it succeeded, returns
     *   SAVED_NEW or SAVED_UPDATED.
     *
     * @throws \Drupal\locale\StringStorageException
     *   If the string is not suitable for this storage, an exception is thrown.
     */
    protected function dbStringUpdate($string) {
        if ($string->isSource()) {
            $values = $string->getValues([
                'source',
                'context',
                'version',
            ]);
        }
        elseif ($string->isTranslation()) {
            $values = $string->getValues([
                'translation',
                'customized',
            ]);
        }
        if (!empty($values) && ($keys = $this->dbStringKeys($string))) {
            return $this->connection
                ->merge($this->dbStringTable($string), $this->options)
                ->keys($keys)
                ->fields($values)
                ->execute();
        }
        else {
            throw new StringStorageException('The string cannot be updated: ' . $string->getString());
        }
    }
    
    /**
     * Creates delete query.
     *
     * @param string $table
     *   The table name.
     * @param array $keys
     *   Array with object keys indexed by field name.
     *
     * @return \Drupal\Core\Database\Query\Delete
     *   Returns a new Delete object for the injected database connection.
     */
    protected function dbDelete($table, $keys) {
        $query = $this->connection
            ->delete($table, $this->options);
        foreach ($keys as $field => $value) {
            $query->condition($field, $value);
        }
        return $query;
    }
    
    /**
     * Executes an arbitrary SELECT query string with the injected options.
     */
    protected function dbExecute($query, array $args = []) {
        return $this->connection
            ->query($query, $args, $this->options);
    }

}

Classes

Title Deprecated Summary
StringDatabaseStorage Defines a class to store localized strings in the database.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.