541 lines
16 KiB
PHP
541 lines
16 KiB
PHP
<?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 = array();
|
|
|
|
/**
|
|
* 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 = array()) {
|
|
$this->connection = $connection;
|
|
$this->options = $options;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getStrings(array $conditions = array(), array $options = array()) {
|
|
return $this->dbStringLoad($conditions, $options, 'Drupal\locale\SourceString');
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getTranslations(array $conditions = array(), array $options = array()) {
|
|
return $this->dbStringLoad($conditions, array('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, array('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 = array()) {
|
|
$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', array('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(array('sid' => $string->getId(), 'type' => $type, 'name' => $name))
|
|
->fields(array('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(array('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, array('fields' => array('lid')))->execute()->fetchCol();
|
|
if ($lids) {
|
|
$this->dbDelete('locales_target', array('lid' => $lids))->execute();
|
|
$this->dbDelete('locales_source', array('lid' => $lids))->execute();
|
|
$this->dbDelete('locales_location', array('sid' => $lids))->execute();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function deleteTranslations($conditions) {
|
|
$this->dbDelete('locales_target', $conditions)->execute();
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function createString($values = array()) {
|
|
return new SourceString($values + array('storage' => $this));
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function createTranslation($values = array()) {
|
|
return new TranslationString($values + array(
|
|
'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, array('language', 'translation', 'customized'))) {
|
|
return 't';
|
|
}
|
|
elseif (in_array($field, array('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 = array('lid');
|
|
}
|
|
elseif ($string->isTranslation()) {
|
|
$keys = array('lid', 'language');
|
|
}
|
|
if (!empty($keys) && ($values = $string->getValues($keys)) && count($keys) == count($values)) {
|
|
return $values;
|
|
}
|
|
else {
|
|
return array();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 = array();
|
|
$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 = array()) {
|
|
// 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", array(
|
|
':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', array('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', array('sid'));
|
|
foreach (array('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(array('context' => '', 'version' => 'none'), FALSE);
|
|
$fields = $string->getValues(array('source', 'context', 'version'));
|
|
}
|
|
elseif ($string->isTranslation()) {
|
|
$string->setValues(array('customized' => 0), FALSE);
|
|
$fields = $string->getValues(array('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(array('source', 'context', 'version'));
|
|
}
|
|
elseif ($string->isTranslation()) {
|
|
$values = $string->getValues(array('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 = array()) {
|
|
return $this->connection->query($query, $args, $this->options);
|
|
}
|
|
|
|
}
|