1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
15: namespace Cake\ORM\Behavior;
16:
17: use ArrayObject;
18: use Cake\Collection\Collection;
19: use Cake\Datasource\EntityInterface;
20: use Cake\Event\Event;
21: use Cake\I18n\I18n;
22: use Cake\ORM\Behavior;
23: use Cake\ORM\Entity;
24: use Cake\ORM\Locator\LocatorAwareTrait;
25: use Cake\ORM\Query;
26: use Cake\ORM\Table;
27: use Cake\Utility\Inflector;
28:
29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40:
41: class TranslateBehavior extends Behavior
42: {
43:
44: use LocatorAwareTrait;
45:
46: 47: 48: 49: 50:
51: protected $_table;
52:
53: 54: 55: 56: 57: 58:
59: protected $_locale;
60:
61: 62: 63: 64: 65:
66: protected $_translationTable;
67:
68: 69: 70: 71: 72: 73: 74:
75: protected $_defaultConfig = [
76: 'implementedFinders' => ['translations' => 'findTranslations'],
77: 'implementedMethods' => ['locale' => 'locale'],
78: 'fields' => [],
79: 'translationTable' => 'I18n',
80: 'defaultLocale' => '',
81: 'referenceName' => '',
82: 'allowEmptyTranslations' => true,
83: 'onlyTranslated' => false,
84: 'strategy' => 'subquery',
85: 'tableLocator' => null
86: ];
87:
88: 89: 90: 91: 92: 93:
94: public function __construct(Table $table, array $config = [])
95: {
96: $config += [
97: 'defaultLocale' => I18n::defaultLocale(),
98: 'referenceName' => $this->_referenceName($table)
99: ];
100:
101: if (isset($config['tableLocator'])) {
102: $this->_tableLocator = $config['tableLocator'];
103: }
104:
105: parent::__construct($table, $config);
106: }
107:
108: 109: 110: 111: 112: 113:
114: public function initialize(array $config)
115: {
116: $this->_translationTable = $this->tableLocator()->get($this->_config['translationTable']);
117:
118: $this->setupFieldAssociations(
119: $this->_config['fields'],
120: $this->_config['translationTable'],
121: $this->_config['referenceName'],
122: $this->_config['strategy']
123: );
124: }
125:
126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139:
140: public function setupFieldAssociations($fields, $table, $model, $strategy)
141: {
142: $targetAlias = $this->_translationTable->alias();
143: $alias = $this->_table->alias();
144: $filter = $this->_config['onlyTranslated'];
145: $tableLocator = $this->tableLocator();
146:
147: foreach ($fields as $field) {
148: $name = $alias . '_' . $field . '_translation';
149:
150: if (!$tableLocator->exists($name)) {
151: $fieldTable = $tableLocator->get($name, [
152: 'className' => $table,
153: 'alias' => $name,
154: 'table' => $this->_translationTable->table()
155: ]);
156: } else {
157: $fieldTable = $tableLocator->get($name);
158: }
159:
160: $conditions = [
161: $name . '.model' => $model,
162: $name . '.field' => $field,
163: ];
164: if (!$this->_config['allowEmptyTranslations']) {
165: $conditions[$name . '.content !='] = '';
166: }
167:
168: $this->_table->hasOne($name, [
169: 'targetTable' => $fieldTable,
170: 'foreignKey' => 'foreign_key',
171: 'joinType' => $filter ? 'INNER' : 'LEFT',
172: 'conditions' => $conditions,
173: 'propertyName' => $field . '_translation'
174: ]);
175: }
176:
177: $conditions = ["$targetAlias.model" => $model];
178: if (!$this->_config['allowEmptyTranslations']) {
179: $conditions["$targetAlias.content !="] = '';
180: }
181:
182: $this->_table->hasMany($targetAlias, [
183: 'className' => $table,
184: 'foreignKey' => 'foreign_key',
185: 'strategy' => $strategy,
186: 'conditions' => $conditions,
187: 'propertyName' => '_i18n',
188: 'dependent' => true
189: ]);
190: }
191:
192: 193: 194: 195: 196: 197: 198: 199: 200: 201:
202: public function beforeFind(Event $event, Query $query, $options)
203: {
204: $locale = $this->locale();
205:
206: if ($locale === $this->config('defaultLocale')) {
207: return;
208: }
209:
210: $conditions = function ($field, $locale, $query, $select) {
211: return function ($q) use ($field, $locale, $query, $select) {
212: $q->where([$q->repository()->aliasField('locale') => $locale]);
213:
214: if ($query->autoFields() ||
215: in_array($field, $select, true) ||
216: in_array($this->_table->aliasField($field), $select, true)
217: ) {
218: $q->select(['id', 'content']);
219: }
220:
221: return $q;
222: };
223: };
224:
225: $contain = [];
226: $fields = $this->_config['fields'];
227: $alias = $this->_table->alias();
228: $select = $query->clause('select');
229:
230: $changeFilter = isset($options['filterByCurrentLocale']) &&
231: $options['filterByCurrentLocale'] !== $this->_config['onlyTranslated'];
232:
233: foreach ($fields as $field) {
234: $name = $alias . '_' . $field . '_translation';
235:
236: $contain[$name]['queryBuilder'] = $conditions(
237: $field,
238: $locale,
239: $query,
240: $select
241: );
242:
243: if ($changeFilter) {
244: $filter = $options['filterByCurrentLocale'] ? 'INNER' : 'LEFT';
245: $contain[$name]['joinType'] = $filter;
246: }
247: }
248:
249: $query->contain($contain);
250: $query->formatResults(function ($results) use ($locale) {
251: return $this->_rowMapper($results, $locale);
252: }, $query::PREPEND);
253: }
254:
255: 256: 257: 258: 259: 260: 261: 262: 263:
264: public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options)
265: {
266: $locale = $entity->get('_locale') ?: $this->locale();
267: $newOptions = [$this->_translationTable->alias() => ['validate' => false]];
268: $options['associated'] = $newOptions + $options['associated'];
269:
270: $this->_bundleTranslatedFields($entity);
271: $bundled = $entity->get('_i18n') ?: [];
272:
273: if ($locale === $this->config('defaultLocale')) {
274: return;
275: }
276:
277: $values = $entity->extract($this->_config['fields'], true);
278: $fields = array_keys($values);
279:
280: if (empty($fields)) {
281: return;
282: }
283:
284: $primaryKey = (array)$this->_table->primaryKey();
285: $key = $entity->get(current($primaryKey));
286: $model = $this->_config['referenceName'];
287:
288: $preexistent = $this->_translationTable->find()
289: ->select(['id', 'field'])
290: ->where(['field IN' => $fields, 'locale' => $locale, 'foreign_key' => $key, 'model' => $model])
291: ->bufferResults(false)
292: ->indexBy('field');
293:
294: $modified = [];
295: foreach ($preexistent as $field => $translation) {
296: $translation->set('content', $values[$field]);
297: $modified[$field] = $translation;
298: }
299:
300: $new = array_diff_key($values, $modified);
301: foreach ($new as $field => $content) {
302: $new[$field] = new Entity(compact('locale', 'field', 'content', 'model'), [
303: 'useSetters' => false,
304: 'markNew' => true
305: ]);
306: }
307:
308: $entity->set('_i18n', array_merge($bundled, array_values($modified + $new)));
309: $entity->set('_locale', $locale, ['setter' => false]);
310: $entity->dirty('_locale', false);
311:
312: foreach ($fields as $field) {
313: $entity->dirty($field, false);
314: }
315: }
316:
317: 318: 319: 320: 321: 322: 323:
324: public function afterSave(Event $event, EntityInterface $entity)
325: {
326: $entity->unsetProperty('_i18n');
327: }
328:
329: 330: 331: 332: 333: 334: 335: 336:
337: public function locale($locale = null)
338: {
339: if ($locale === null) {
340: return $this->_locale ?: I18n::locale();
341: }
342:
343: return $this->_locale = (string)$locale;
344: }
345:
346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 361: 362: 363: 364: 365: 366: 367:
368: public function findTranslations(Query $query, array $options)
369: {
370: $locales = isset($options['locales']) ? $options['locales'] : [];
371: $targetAlias = $this->_translationTable->alias();
372:
373: return $query
374: ->contain([$targetAlias => function ($q) use ($locales, $targetAlias) {
375: if ($locales) {
376: $q->where(["$targetAlias.locale IN" => $locales]);
377: }
378:
379: return $q;
380: }])
381: ->formatResults([$this, 'groupTranslations'], $query::PREPEND);
382: }
383:
384: 385: 386: 387: 388: 389: 390: 391: 392: 393: 394:
395: protected function _referenceName(Table $table)
396: {
397: $name = namespaceSplit(get_class($table));
398: $name = substr(end($name), 0, -5);
399: if (empty($name)) {
400: $name = $table->table() ?: $table->alias();
401: $name = Inflector::camelize($name);
402: }
403:
404: return $name;
405: }
406:
407: 408: 409: 410: 411: 412: 413: 414:
415: protected function _rowMapper($results, $locale)
416: {
417: return $results->map(function ($row) use ($locale) {
418: if ($row === null) {
419: return $row;
420: }
421: $hydrated = !is_array($row);
422:
423: foreach ($this->_config['fields'] as $field) {
424: $name = $field . '_translation';
425: $translation = isset($row[$name]) ? $row[$name] : null;
426:
427: if ($translation === null || $translation === false) {
428: unset($row[$name]);
429: continue;
430: }
431:
432: $content = isset($translation['content']) ? $translation['content'] : null;
433: if ($content !== null) {
434: $row[$field] = $content;
435: }
436:
437: unset($row[$name]);
438: }
439:
440: $row['_locale'] = $locale;
441: if ($hydrated) {
442: $row->clean();
443: }
444:
445: return $row;
446: });
447: }
448:
449: 450: 451: 452: 453: 454: 455:
456: public function groupTranslations($results)
457: {
458: return $results->map(function ($row) {
459: if (!$row instanceof EntityInterface) {
460: return $row;
461: }
462: $translations = (array)$row->get('_i18n');
463: if (empty($translations) && $row->get('_translations')) {
464: return $row;
465: }
466: $grouped = new Collection($translations);
467:
468: $result = [];
469: foreach ($grouped->combine('field', 'content', 'locale') as $locale => $keys) {
470: $entityClass = $this->_table->entityClass();
471: $translation = new $entityClass($keys + ['locale' => $locale], [
472: 'markNew' => false,
473: 'useSetters' => false,
474: 'markClean' => true
475: ]);
476: $result[$locale] = $translation;
477: }
478:
479: $options = ['setter' => false, 'guard' => false];
480: $row->set('_translations', $result, $options);
481: unset($row['_i18n']);
482: $row->clean();
483:
484: return $row;
485: });
486: }
487:
488: 489: 490: 491: 492: 493: 494: 495:
496: protected function _bundleTranslatedFields($entity)
497: {
498: $translations = (array)$entity->get('_translations');
499:
500: if (empty($translations) && !$entity->dirty('_translations')) {
501: return;
502: }
503:
504: $fields = $this->_config['fields'];
505: $primaryKey = (array)$this->_table->primaryKey();
506: $key = $entity->get(current($primaryKey));
507: $find = [];
508:
509: foreach ($translations as $lang => $translation) {
510: foreach ($fields as $field) {
511: if (!$translation->dirty($field)) {
512: continue;
513: }
514: $find[] = ['locale' => $lang, 'field' => $field, 'foreign_key' => $key];
515: $contents[] = new Entity(['content' => $translation->get($field)], [
516: 'useSetters' => false
517: ]);
518: }
519: }
520:
521: if (empty($find)) {
522: return;
523: }
524:
525: $results = $this->_findExistingTranslations($find);
526:
527: foreach ($find as $i => $translation) {
528: if (!empty($results[$i])) {
529: $contents[$i]->set('id', $results[$i], ['setter' => false]);
530: $contents[$i]->isNew(false);
531: } else {
532: $translation['model'] = $this->_config['referenceName'];
533: $contents[$i]->set($translation, ['setter' => false, 'guard' => false]);
534: $contents[$i]->isNew(true);
535: }
536: }
537:
538: $entity->set('_i18n', $contents);
539: }
540:
541: 542: 543: 544: 545: 546: 547:
548: protected function _findExistingTranslations($ruleSet)
549: {
550: $association = $this->_table->association($this->_translationTable->alias());
551:
552: $query = $association->find()
553: ->select(['id', 'num' => 0])
554: ->where(current($ruleSet))
555: ->hydrate(false)
556: ->bufferResults(false);
557:
558: unset($ruleSet[0]);
559: foreach ($ruleSet as $i => $conditions) {
560: $q = $association->find()
561: ->select(['id', 'num' => $i])
562: ->where($conditions);
563: $query->unionAll($q);
564: }
565:
566: return $query->combine('num', 'id')->toArray();
567: }
568: }
569: