Version 1
[yaffs-website] / web / core / modules / locale / src / StringDatabaseStorage.php
diff --git a/web/core/modules/locale/src/StringDatabaseStorage.php b/web/core/modules/locale/src/StringDatabaseStorage.php
new file mode 100644 (file)
index 0000000..57358dc
--- /dev/null
@@ -0,0 +1,540 @@
+<?php
+
+namespace Drupal\locale;
+
+use Drupal\Core\Database\Connection;
+
+/**
+ * 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(db_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 = db_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, '%' . db_like($string) . '%', 'LIKE');
+      }
+    }
+
+    if (!empty($options['pager limit'])) {
+      $query = $query->extend('Drupal\Core\Database\Query\PagerSelectExtender')->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);
+  }
+
+}