Seditio Source
Root |
./othercms/croogo-4.0.7/vendor/cakephp/cakephp/src/ORM/Behavior/TranslateBehavior.php
<?php
/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link          https://cakephp.org CakePHP(tm) Project
 * @since         3.0.0
 * @license       https://opensource.org/licenses/mit-license.php MIT License
 */
namespace Cake\ORM\Behavior;

use
ArrayObject;
use
Cake\Collection\Collection;
use
Cake\Datasource\EntityInterface;
use
Cake\Datasource\QueryInterface;
use
Cake\Event\Event;
use
Cake\I18n\I18n;
use
Cake\ORM\Behavior;
use
Cake\ORM\Entity;
use
Cake\ORM\Locator\LocatorAwareTrait;
use
Cake\ORM\PropertyMarshalInterface;
use
Cake\ORM\Query;
use
Cake\ORM\Table;
use
Cake\Utility\Inflector;

/**
 * This behavior provides a way to translate dynamic data by keeping translations
 * in a separate table linked to the original record from another one. Translated
 * fields can be configured to override those in the main table when fetched or
 * put aside into another property for the same entity.
 *
 * If you wish to override fields, you need to call the `locale` method in this
 * behavior for setting the language you want to fetch from the translations table.
 *
 * If you want to bring all or certain languages for each of the fetched records,
 * you can use the custom `translations` finders that is exposed to the table.
 */
class TranslateBehavior extends Behavior implements PropertyMarshalInterface
{
    use
LocatorAwareTrait;

   
/**
     * Table instance
     *
     * @var \Cake\ORM\Table
     */
   
protected $_table;

   
/**
     * The locale name that will be used to override fields in the bound table
     * from the translations table
     *
     * @var string
     */
   
protected $_locale;

   
/**
     * Instance of Table responsible for translating
     *
     * @var \Cake\ORM\Table
     */
   
protected $_translationTable;

   
/**
     * Default config
     *
     * These are merged with user-provided configuration when the behavior is used.
     *
     * @var array
     */
   
protected $_defaultConfig = [
       
'implementedFinders' => ['translations' => 'findTranslations'],
       
'implementedMethods' => [
           
'setLocale' => 'setLocale',
           
'getLocale' => 'getLocale',
           
'locale' => 'locale',
           
'translationField' => 'translationField',
        ],
       
'fields' => [],
       
'translationTable' => 'I18n',
       
'defaultLocale' => '',
       
'referenceName' => '',
       
'allowEmptyTranslations' => true,
       
'onlyTranslated' => false,
       
'strategy' => 'subquery',
       
'tableLocator' => null,
       
'validator' => false,
    ];

   
/**
     * Constructor
     *
     * @param \Cake\ORM\Table $table The table this behavior is attached to.
     * @param array $config The config for this behavior.
     */
   
public function __construct(Table $table, array $config = [])
    {
       
$config += [
           
'defaultLocale' => I18n::getDefaultLocale(),
           
'referenceName' => $this->_referenceName($table),
        ];

        if (isset(
$config['tableLocator'])) {
           
$this->_tableLocator = $config['tableLocator'];
        } else {
           
$this->_tableLocator = $table->associations()->getTableLocator();
        }

       
parent::__construct($table, $config);
    }

   
/**
     * Initialize hook
     *
     * @param array $config The config for this behavior.
     * @return void
     */
   
public function initialize(array $config)
    {
       
$this->_translationTable = $this->getTableLocator()->get($this->_config['translationTable']);

       
$this->setupFieldAssociations(
           
$this->_config['fields'],
           
$this->_config['translationTable'],
           
$this->_config['referenceName'],
           
$this->_config['strategy']
        );
    }

   
/**
     * Creates the associations between the bound table and every field passed to
     * this method.
     *
     * Additionally it creates a `i18n` HasMany association that will be
     * used for fetching all translations for each record in the bound table
     *
     * @param array $fields list of fields to create associations for
     * @param string $table the table name to use for storing each field translation
     * @param string $model the model field value
     * @param string $strategy the strategy used in the _i18n association
     *
     * @return void
     */
   
public function setupFieldAssociations($fields, $table, $model, $strategy)
    {
       
$targetAlias = $this->_translationTable->getAlias();
       
$alias = $this->_table->getAlias();
       
$filter = $this->_config['onlyTranslated'];
       
$tableLocator = $this->getTableLocator();

        foreach (
$fields as $field) {
           
$name = $alias . '_' . $field . '_translation';

            if (!
$tableLocator->exists($name)) {
               
$fieldTable = $tableLocator->get($name, [
                   
'className' => $table,
                   
'alias' => $name,
                   
'table' => $this->_translationTable->getTable(),
                ]);
            } else {
               
$fieldTable = $tableLocator->get($name);
            }

           
$conditions = [
               
$name . '.model' => $model,
               
$name . '.field' => $field,
            ];
            if (!
$this->_config['allowEmptyTranslations']) {
               
$conditions[$name . '.content !='] = '';
            }

           
$this->_table->hasOne($name, [
               
'targetTable' => $fieldTable,
               
'foreignKey' => 'foreign_key',
               
'joinType' => $filter ? QueryInterface::JOIN_TYPE_INNER : QueryInterface::JOIN_TYPE_LEFT,
               
'conditions' => $conditions,
               
'propertyName' => $field . '_translation',
            ]);
        }

       
$conditions = ["$targetAlias.model" => $model];
        if (!
$this->_config['allowEmptyTranslations']) {
           
$conditions["$targetAlias.content !="] = '';
        }

       
$this->_table->hasMany($targetAlias, [
           
'className' => $table,
           
'foreignKey' => 'foreign_key',
           
'strategy' => $strategy,
           
'conditions' => $conditions,
           
'propertyName' => '_i18n',
           
'dependent' => true,
        ]);
    }

   
/**
     * Callback method that listens to the `beforeFind` event in the bound
     * table. It modifies the passed query by eager loading the translated fields
     * and adding a formatter to copy the values into the main table records.
     *
     * @param \Cake\Event\Event $event The beforeFind event that was fired.
     * @param \Cake\ORM\Query $query Query
     * @param \ArrayObject $options The options for the query
     * @return void
     */
   
public function beforeFind(Event $event, Query $query, $options)
    {
       
$locale = $this->getLocale();

        if (
$locale === $this->getConfig('defaultLocale')) {
            return;
        }

       
$conditions = function ($field, $locale, $query, $select) {
            return function (
$q) use ($field, $locale, $query, $select) {
               
/** @var \Cake\Datasource\QueryInterface $q */
               
$q->where([$q->getRepository()->aliasField('locale') => $locale]);

               
/** @var \Cake\ORM\Query $query */
               
if (
                   
$query->isAutoFieldsEnabled() ||
                   
in_array($field, $select, true) ||
                   
in_array($this->_table->aliasField($field), $select, true)
                ) {
                   
$q->select(['id', 'content']);
                }

                return
$q;
            };
        };

       
$contain = [];
       
$fields = $this->_config['fields'];
       
$alias = $this->_table->getAlias();
       
$select = $query->clause('select');

       
$changeFilter = isset($options['filterByCurrentLocale']) &&
           
$options['filterByCurrentLocale'] !== $this->_config['onlyTranslated'];

        foreach (
$fields as $field) {
           
$name = $alias . '_' . $field . '_translation';

           
$contain[$name]['queryBuilder'] = $conditions(
               
$field,
               
$locale,
               
$query,
               
$select
           
);

            if (
$changeFilter) {
               
$filter = $options['filterByCurrentLocale'] ? QueryInterface::JOIN_TYPE_INNER : QueryInterface::JOIN_TYPE_LEFT;
               
$contain[$name]['joinType'] = $filter;
            }
        }

       
$query->contain($contain);
       
$query->formatResults(function ($results) use ($locale) {
            return
$this->_rowMapper($results, $locale);
        },
$query::PREPEND);
    }

   
/**
     * Modifies the entity before it is saved so that translated fields are persisted
     * in the database too.
     *
     * @param \Cake\Event\Event $event The beforeSave event that was fired
     * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved
     * @param \ArrayObject $options the options passed to the save method
     * @return void
     */
   
public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options)
    {
       
$locale = $entity->get('_locale') ?: $this->getLocale();
       
$newOptions = [$this->_translationTable->getAlias() => ['validate' => false]];
       
$options['associated'] = $newOptions + $options['associated'];

       
// Check early if empty translations are present in the entity.
        // If this is the case, unset them to prevent persistence.
        // This only applies if $this->_config['allowEmptyTranslations'] is false
       
if ($this->_config['allowEmptyTranslations'] === false) {
           
$this->_unsetEmptyFields($entity);
        }

       
$this->_bundleTranslatedFields($entity);
       
$bundled = $entity->get('_i18n') ?: [];
       
$noBundled = count($bundled) === 0;

       
// No additional translation records need to be saved,
        // as the entity is in the default locale.
       
if ($noBundled && $locale === $this->getConfig('defaultLocale')) {
            return;
        }

       
$values = $entity->extract($this->_config['fields'], true);
       
$fields = array_keys($values);
       
$noFields = empty($fields);

       
// If there are no fields and no bundled translations, or both fields
        // in the default locale and bundled translations we can
        // skip the remaining logic as its not necessary.
       
if ($noFields && $noBundled || ($fields && $bundled)) {
            return;
        }

       
$primaryKey = (array)$this->_table->getPrimaryKey();
       
$key = $entity->get(current($primaryKey));

       
// When we have no key and bundled translations, we
        // need to mark the entity dirty so the root
        // entity persists.
       
if ($noFields && $bundled && !$key) {
            foreach (
$this->_config['fields'] as $field) {
               
$entity->setDirty($field, true);
            }

            return;
        }

        if (
$noFields) {
            return;
        }

       
$model = $this->_config['referenceName'];
       
$preexistent = $this->_translationTable->find()
            ->
select(['id', 'field'])
            ->
where([
               
'field IN' => $fields,
               
'locale' => $locale,
               
'foreign_key' => $key,
               
'model' => $model,
            ])
            ->
disableBufferedResults()
            ->
all()
            ->
indexBy('field');

       
$modified = [];
        foreach (
$preexistent as $field => $translation) {
           
$translation->set('content', $values[$field]);
           
$modified[$field] = $translation;
        }

       
$new = array_diff_key($values, $modified);
        foreach (
$new as $field => $content) {
           
$new[$field] = new Entity(compact('locale', 'field', 'content', 'model'), [
               
'useSetters' => false,
               
'markNew' => true,
            ]);
        }

       
$entity->set('_i18n', array_merge($bundled, array_values($modified + $new)));
       
$entity->set('_locale', $locale, ['setter' => false]);
       
$entity->setDirty('_locale', false);

        foreach (
$fields as $field) {
           
$entity->setDirty($field, false);
        }
    }

   
/**
     * Unsets the temporary `_i18n` property after the entity has been saved
     *
     * @param \Cake\Event\Event $event The beforeSave event that was fired
     * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved
     * @return void
     */
   
public function afterSave(Event $event, EntityInterface $entity)
    {
       
$entity->unsetProperty('_i18n');
    }

   
/**
     * {@inheritDoc}
     *
     * Add in `_translations` marshalling handlers. You can disable marshalling
     * of translations by setting `'translations' => false` in the options
     * provided to `Table::newEntity()` or `Table::patchEntity()`.
     */
   
public function buildMarshalMap($marshaller, $map, $options)
    {
        if (isset(
$options['translations']) && !$options['translations']) {
            return [];
        }

        return [
           
'_translations' => function ($value, $entity) use ($marshaller, $options) {
               
/** @var \Cake\Datasource\EntityInterface $entity */
               
$translations = $entity->get('_translations');
                foreach (
$this->_config['fields'] as $field) {
                   
$options['validate'] = $this->_config['validator'];
                   
$errors = [];
                    if (!
is_array($value)) {
                        return
null;
                    }
                    foreach (
$value as $language => $fields) {
                        if (!isset(
$translations[$language])) {
                           
$translations[$language] = $this->_table->newEntity();
                        }
                       
$marshaller->merge($translations[$language], $fields, $options);
                        if ((bool)
$translations[$language]->getErrors()) {
                           
$errors[$language] = $translations[$language]->getErrors();
                        }
                    }
                   
// Set errors into the root entity, so validation errors
                    // match the original form data position.
                   
$entity->setErrors($errors);
                }

                return
$translations;
            },
        ];
    }

   
/**
     * Sets the locale that should be used for all future find and save operations on
     * the table where this behavior is attached to.
     *
     * When fetching records, the behavior will include the content for the locale set
     * via this method, and likewise when saving data, it will save the data in that
     * locale.
     *
     * Note that in case an entity has a `_locale` property set, that locale will win
     * over the locale set via this method (and over the globally configured one for
     * that matter)!
     *
     * @param string|null $locale The locale to use for fetching and saving records. Pass `null`
     * in order to unset the current locale, and to make the behavior fall back to using the
     * globally configured locale.
     * @return $this
     * @see \Cake\ORM\Behavior\TranslateBehavior::getLocale()
     * @link https://book.cakephp.org/3/en/orm/behaviors/translate.html#retrieving-one-language-without-using-i18n-locale
     * @link https://book.cakephp.org/3/en/orm/behaviors/translate.html#saving-in-another-language
     */
   
public function setLocale($locale)
    {
       
$this->_locale = $locale;

        return
$this;
    }

   
/**
     * Returns the current locale.
     *
     * If no locale has been explicitly set via `setLocale()`, this method will return
     * the currently configured global locale.
     *
     * @return string
     * @see \Cake\I18n\I18n::getLocale()
     * @see \Cake\ORM\Behavior\TranslateBehavior::setLocale()
     */
   
public function getLocale()
    {
        return
$this->_locale ?: I18n::getLocale();
    }

   
/**
     * Sets all future finds for the bound table to also fetch translated fields for
     * the passed locale. If no value is passed, it returns the currently configured
     * locale
     *
     * @deprecated 3.6.0 Use setLocale()/getLocale() instead.
     * @param string|null $locale The locale to use for fetching translated records
     * @return string
     */
   
public function locale($locale = null)
    {
       
deprecationWarning(
           
get_called_class() . '::locale() is deprecated. ' .
           
'Use setLocale()/getLocale() instead.'
       
);

        if (
$locale !== null) {
           
$this->setLocale($locale);
        }

        return
$this->getLocale();
    }

   
/**
     * Returns a fully aliased field name for translated fields.
     *
     * If the requested field is configured as a translation field, the `content`
     * field with an alias of a corresponding association is returned. Table-aliased
     * field name is returned for all other fields.
     *
     * @param string $field Field name to be aliased.
     * @return string
     */
   
public function translationField($field)
    {
       
$table = $this->_table;
        if (
$this->getLocale() === $this->getConfig('defaultLocale')) {
            return
$table->aliasField($field);
        }
       
$associationName = $table->getAlias() . '_' . $field . '_translation';

        if (
$table->associations()->has($associationName)) {
            return
$associationName . '.content';
        }

        return
$table->aliasField($field);
    }

   
/**
     * Custom finder method used to retrieve all translations for the found records.
     * Fetched translations can be filtered by locale by passing the `locales` key
     * in the options array.
     *
     * Translated values will be found for each entity under the property `_translations`,
     * containing an array indexed by locale name.
     *
     * ### Example:
     *
     * ```
     * $article = $articles->find('translations', ['locales' => ['eng', 'deu'])->first();
     * $englishTranslatedFields = $article->get('_translations')['eng'];
     * ```
     *
     * If the `locales` array is not passed, it will bring all translations found
     * for each record.
     *
     * @param \Cake\ORM\Query $query The original query to modify
     * @param array $options Options
     * @return \Cake\ORM\Query
     */
   
public function findTranslations(Query $query, array $options)
    {
       
$locales = isset($options['locales']) ? $options['locales'] : [];
       
$targetAlias = $this->_translationTable->getAlias();

        return
$query
           
->contain([$targetAlias => function ($query) use ($locales, $targetAlias) {
                if (
$locales) {
                   
/** @var \Cake\Datasource\QueryInterface $query */
                   
$query->where(["$targetAlias.locale IN" => $locales]);
                }

                return
$query;
            }])
            ->
formatResults([$this, 'groupTranslations'], $query::PREPEND);
    }

   
/**
     * Determine the reference name to use for a given table
     *
     * The reference name is usually derived from the class name of the table object
     * (PostsTable -> Posts), however for autotable instances it is derived from
     * the database table the object points at - or as a last resort, the alias
     * of the autotable instance.
     *
     * @param \Cake\ORM\Table $table The table class to get a reference name for.
     * @return string
     */
   
protected function _referenceName(Table $table)
    {
       
$name = namespaceSplit(get_class($table));
       
$name = substr(end($name), 0, -5);
        if (empty(
$name)) {
           
$name = $table->getTable() ?: $table->getAlias();
           
$name = Inflector::camelize($name);
        }

        return
$name;
    }

   
/**
     * Modifies the results from a table find in order to merge the translated fields
     * into each entity for a given locale.
     *
     * @param \Cake\Datasource\ResultSetInterface $results Results to map.
     * @param string $locale Locale string
     * @return \Cake\Collection\CollectionInterface
     */
   
protected function _rowMapper($results, $locale)
    {
        return
$results->map(function ($row) use ($locale) {
            if (
$row === null) {
                return
$row;
            }
           
$hydrated = !is_array($row);

            foreach (
$this->_config['fields'] as $field) {
               
$name = $field . '_translation';
               
$translation = isset($row[$name]) ? $row[$name] : null;

                if (
$translation === null || $translation === false) {
                    unset(
$row[$name]);
                    continue;
                }

               
$content = isset($translation['content']) ? $translation['content'] : null;
                if (
$content !== null) {
                   
$row[$field] = $content;
                }

                unset(
$row[$name]);
            }

           
$row['_locale'] = $locale;
            if (
$hydrated) {
               
/** @var \Cake\Datasource\EntityInterface $row */
               
$row->clean();
            }

            return
$row;
        });
    }

   
/**
     * Modifies the results from a table find in order to merge full translation records
     * into each entity under the `_translations` key
     *
     * @param \Cake\Datasource\ResultSetInterface $results Results to modify.
     * @return \Cake\Collection\CollectionInterface
     */
   
public function groupTranslations($results)
    {
        return
$results->map(function ($row) {
            if (!
$row instanceof EntityInterface) {
                return
$row;
            }
           
$translations = (array)$row->get('_i18n');
            if (empty(
$translations) && $row->get('_translations')) {
                return
$row;
            }
           
$grouped = new Collection($translations);

           
$result = [];
            foreach (
$grouped->combine('field', 'content', 'locale') as $locale => $keys) {
               
$entityClass = $this->_table->getEntityClass();
               
$translation = new $entityClass($keys + ['locale' => $locale], [
                   
'markNew' => false,
                   
'useSetters' => false,
                   
'markClean' => true,
                ]);
               
$result[$locale] = $translation;
            }

           
$options = ['setter' => false, 'guard' => false];
           
$row->set('_translations', $result, $options);
            unset(
$row['_i18n']);
           
$row->clean();

            return
$row;
        });
    }

   
/**
     * Helper method used to generated multiple translated field entities
     * out of the data found in the `_translations` property in the passed
     * entity. The result will be put into its `_i18n` property
     *
     * @param \Cake\Datasource\EntityInterface $entity Entity
     * @return void
     */
   
protected function _bundleTranslatedFields($entity)
    {
       
$translations = (array)$entity->get('_translations');

        if (empty(
$translations) && !$entity->isDirty('_translations')) {
            return;
        }

       
$fields = $this->_config['fields'];
       
$primaryKey = (array)$this->_table->getPrimaryKey();
       
$key = $entity->get(current($primaryKey));
       
$find = [];
       
$contents = [];

        foreach (
$translations as $lang => $translation) {
            foreach (
$fields as $field) {
                if (!
$translation->isDirty($field)) {
                    continue;
                }
               
$find[] = ['locale' => $lang, 'field' => $field, 'foreign_key' => $key];
               
$contents[] = new Entity(['content' => $translation->get($field)], [
                   
'useSetters' => false,
                ]);
            }
        }

        if (empty(
$find)) {
            return;
        }

       
$results = $this->_findExistingTranslations($find);

        foreach (
$find as $i => $translation) {
            if (!empty(
$results[$i])) {
               
$contents[$i]->set('id', $results[$i], ['setter' => false]);
               
$contents[$i]->isNew(false);
            } else {
               
$translation['model'] = $this->_config['referenceName'];
               
$contents[$i]->set($translation, ['setter' => false, 'guard' => false]);
               
$contents[$i]->isNew(true);
            }
        }

       
$entity->set('_i18n', $contents);
    }

   
/**
     * Unset empty translations to avoid persistence.
     *
     * Should only be called if $this->_config['allowEmptyTranslations'] is false.
     *
     * @param \Cake\Datasource\EntityInterface $entity The entity to check for empty translations fields inside.
     * @return void
     */
   
protected function _unsetEmptyFields(EntityInterface $entity)
    {
       
$translations = (array)$entity->get('_translations');
        foreach (
$translations as $locale => $translation) {
           
$fields = $translation->extract($this->_config['fields'], false);
            foreach (
$fields as $field => $value) {
                if (
strlen($value) === 0) {
                   
$translation->unsetProperty($field);
                }
            }

           
$translation = $translation->extract($this->_config['fields']);

           
// If now, the current locale property is empty,
            // unset it completely.
           
if (empty(array_filter($translation))) {
                unset(
$entity->get('_translations')[$locale]);
            }
        }

       
// If now, the whole _translations property is empty,
        // unset it completely and return
       
if (empty($entity->get('_translations'))) {
           
$entity->unsetProperty('_translations');
        }
    }

   
/**
     * Returns the ids found for each of the condition arrays passed for the translations
     * table. Each records is indexed by the corresponding position to the conditions array
     *
     * @param array $ruleSet an array of arary of conditions to be used for finding each
     * @return array
     */
   
protected function _findExistingTranslations($ruleSet)
    {
       
$association = $this->_table->getAssociation($this->_translationTable->getAlias());

       
$query = $association->find()
            ->
select(['id', 'num' => 0])
            ->
where(current($ruleSet))
            ->
disableHydration()
            ->
disableBufferedResults();

        unset(
$ruleSet[0]);
        foreach (
$ruleSet as $i => $conditions) {
           
$q = $association->find()
                ->
select(['id', 'num' => $i])
                ->
where($conditions);
           
$query->unionAll($q);
        }

        return
$query->all()->combine('num', 'id')->toArray();
    }
}