/var/www/vhosts/ihelp.ro/httpdocs/vendor/cakephp/cakephp/src/ORM/Behavior/Translate
Edit: /var/www/vhosts/ihelp.ro/httpdocs/vendor/cakephp/cakephp/src/ORM/Behavior/Translate/EavStrategy.php (17856B)
*/
protected $_defaultConfig = [
'fields' => [],
'translationTable' => 'I18n',
'defaultLocale' => null,
'referenceName' => null,
'allowEmptyTranslations' => true,
'onlyTranslated' => false,
'strategy' => 'subquery',
'tableLocator' => null,
'validator' => false,
];
/**
* Constructor
*
* @param \Cake\ORM\Table $table The table this strategy is attached to.
* @param array
$config The config for this strategy.
*/
public function __construct(Table $table, array $config = [])
{
if (isset($config['tableLocator'])) {
$this->_tableLocator = $config['tableLocator'];
}
$this->setConfig($config);
$this->table = $table;
$this->translationTable = $this->getTableLocator()->get(
$this->_config['translationTable'],
['allowFallbackClass' => true]
);
$this->setupAssociations();
}
/**
* 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.
*
* @return void
*/
protected function setupAssociations()
{
$fields = $this->_config['fields'];
$table = $this->_config['translationTable'];
$model = $this->_config['referenceName'];
$strategy = $this->_config['strategy'];
$filter = $this->_config['onlyTranslated'];
$targetAlias = $this->translationTable->getAlias();
$alias = $this->table->getAlias();
$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(),
'allowFallbackClass' => true,
]);
} 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 ? Query::JOIN_TYPE_INNER : Query::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\EventInterface $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(EventInterface $event, Query $query, ArrayObject $options)
{
$locale = Hash::get($options, 'locale', $this->getLocale());
if ($locale === $this->getConfig('defaultLocale')) {
return;
}
$conditions = function ($field, $locale, $query, $select) {
return function ($q) use ($field, $locale, $query, $select) {
$q->where([$q->getRepository()->aliasField('locale') => $locale]);
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']
? Query::JOIN_TYPE_INNER
: Query::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\EventInterface $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(EventInterface $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 = [];
if ($key) {
$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);
}
}
/**
* 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(string $field): string
{
$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);
}
/**
* 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) {
/** @var \Cake\Datasource\EntityInterface|array|null $row */
if ($row === null) {
return $row;
}
$hydrated = !is_array($row);
foreach ($this->_config['fields'] as $field) {
$name = $field . '_translation';
$translation = $row[$name] ?? null;
if ($translation === null || $translation === false) {
unset($row[$name]);
continue;
}
$content = $translation['content'] ?? null;
if ($content !== null) {
$row[$field] = $content;
}
unset($row[$name]);
}
$row['_locale'] = $locale;
if ($hydrated) {
/** @psalm-suppress PossiblyInvalidMethodCall */
$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): CollectionInterface
{
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 IS' => $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]->setNew(false);
} else {
$translation['model'] = $this->_config['referenceName'];
$contents[$i]->set($translation, ['setter' => false, 'guard' => false]);
$contents[$i]->setNew(true);
}
}
$entity->set('_i18n', $contents);
}
/**
* 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 array 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();
}
}