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