CakePHP
  • Documentation
    • Book
    • API
    • Videos
    • Logos & Trademarks
  • Business Solutions
  • Swag
  • Road Trip
  • Team
  • Community
    • Community
    • Team
    • Issues (Github)
    • YouTube Channel
    • Get Involved
    • Bakery
    • Featured Resources
    • Newsletter
    • Certification
    • My CakePHP
    • CakeFest
    • Facebook
    • Twitter
    • Help & Support
    • Forum
    • Stack Overflow
    • IRC
    • Slack
    • Paid Support
CakePHP

C CakePHP 3.2 Red Velvet API

  • Overview
  • Tree
  • Deprecated
  • Version:
    • 3.2
      • 3.8
      • 3.7
      • 3.6
      • 3.5
      • 3.4
      • 3.3
      • 3.2
      • 3.1
      • 3.0
      • 2.10
      • 2.9
      • 2.8
      • 2.7
      • 2.6
      • 2.5
      • 2.4
      • 2.3
      • 2.2
      • 2.1
      • 2.0
      • 1.3
      • 1.2

Namespaces

  • Cake
    • Auth
      • Storage
    • Cache
      • Engine
    • Collection
      • Iterator
    • Console
      • Exception
    • Controller
      • Component
      • Exception
    • Core
      • Configure
        • Engine
      • Exception
    • Database
      • Driver
      • Exception
      • Expression
      • Schema
      • Statement
      • Type
    • Datasource
      • Exception
    • Error
    • Event
    • Filesystem
    • Form
    • I18n
      • Formatter
      • Parser
    • Log
      • Engine
    • Mailer
      • Exception
      • Transport
    • Network
      • Exception
      • Http
        • Adapter
        • Auth
        • FormData
      • Session
    • ORM
      • Association
      • Behavior
        • Translate
      • Exception
      • Locator
      • Rule
    • Routing
      • Exception
      • Filter
      • Route
    • Shell
      • Helper
      • Task
    • TestSuite
      • Constraint
      • Fixture
      • Stub
    • Utility
      • Exception
    • Validation
    • View
      • Exception
      • Form
      • Helper
      • Widget
  • None

Classes

  • CounterCacheBehavior
  • TimestampBehavior
  • TranslateBehavior
  • TreeBehavior
  1: <?php
  2: /**
  3:  * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4:  * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5:  *
  6:  * Licensed under The MIT License
  7:  * For full copyright and license information, please see the LICENSE.txt
  8:  * Redistributions of files must retain the above copyright notice.
  9:  *
 10:  * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 11:  * @link          http://cakephp.org CakePHP(tm) Project
 12:  * @since         3.0.0
 13:  * @license       http://www.opensource.org/licenses/mit-license.php MIT License
 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:  * This behavior provides a way to translate dynamic data by keeping translations
 31:  * in a separate table linked to the original record from another one. Translated
 32:  * fields can be configured to override those in the main table when fetched or
 33:  * put aside into another property for the same entity.
 34:  *
 35:  * If you wish to override fields, you need to call the `locale` method in this
 36:  * behavior for setting the language you want to fetch from the translations table.
 37:  *
 38:  * If you want to bring all or certain languages for each of the fetched records,
 39:  * you can use the custom `translations` finders that is exposed to the table.
 40:  */
 41: class TranslateBehavior extends Behavior
 42: {
 43: 
 44:     use LocatorAwareTrait;
 45: 
 46:     /**
 47:      * Table instance
 48:      *
 49:      * @var \Cake\ORM\Table
 50:      */
 51:     protected $_table;
 52: 
 53:     /**
 54:      * The locale name that will be used to override fields in the bound table
 55:      * from the translations table
 56:      *
 57:      * @var string
 58:      */
 59:     protected $_locale;
 60: 
 61:     /**
 62:      * Instance of Table responsible for translating
 63:      *
 64:      * @var \Cake\ORM\Table
 65:      */
 66:     protected $_translationTable;
 67: 
 68:     /**
 69:      * Default config
 70:      *
 71:      * These are merged with user-provided configuration when the behavior is used.
 72:      *
 73:      * @var array
 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:      * Constructor
 90:      *
 91:      * @param \Cake\ORM\Table $table The table this behavior is attached to.
 92:      * @param array $config The config for this behavior.
 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:      * Initialize hook
110:      *
111:      * @param array $config The config for this behavior.
112:      * @return void
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:      * Creates the associations between the bound table and every field passed to
128:      * this method.
129:      *
130:      * Additionally it creates a `i18n` HasMany association that will be
131:      * used for fetching all translations for each record in the bound table
132:      *
133:      * @param array $fields list of fields to create associations for
134:      * @param string $table the table name to use for storing each field translation
135:      * @param string $model the model field value
136:      * @param string $strategy the strategy used in the _i18n association
137:      *
138:      * @return void
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:      * Callback method that listens to the `beforeFind` event in the bound
194:      * table. It modifies the passed query by eager loading the translated fields
195:      * and adding a formatter to copy the values into the main table records.
196:      *
197:      * @param \Cake\Event\Event $event The beforeFind event that was fired.
198:      * @param \Cake\ORM\Query $query Query
199:      * @param \ArrayObject $options The options for the query
200:      * @return void
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:      * Modifies the entity before it is saved so that translated fields are persisted
257:      * in the database too.
258:      *
259:      * @param \Cake\Event\Event $event The beforeSave event that was fired
260:      * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved
261:      * @param \ArrayObject $options the options passed to the save method
262:      * @return void
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:      * Unsets the temporary `_i18n` property after the entity has been saved
319:      *
320:      * @param \Cake\Event\Event $event The beforeSave event that was fired
321:      * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved
322:      * @return void
323:      */
324:     public function afterSave(Event $event, EntityInterface $entity)
325:     {
326:         $entity->unsetProperty('_i18n');
327:     }
328: 
329:     /**
330:      * Sets all future finds for the bound table to also fetch translated fields for
331:      * the passed locale. If no value is passed, it returns the currently configured
332:      * locale
333:      *
334:      * @param string|null $locale The locale to use for fetching translated records
335:      * @return string
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:      * Custom finder method used to retrieve all translations for the found records.
348:      * Fetched translations can be filtered by locale by passing the `locales` key
349:      * in the options array.
350:      *
351:      * Translated values will be found for each entity under the property `_translations`,
352:      * containing an array indexed by locale name.
353:      *
354:      * ### Example:
355:      *
356:      * ```
357:      * $article = $articles->find('translations', ['locales' => ['eng', 'deu'])->first();
358:      * $englishTranslatedFields = $article->get('_translations')['eng'];
359:      * ```
360:      *
361:      * If the `locales` array is not passed, it will bring all translations found
362:      * for each record.
363:      *
364:      * @param \Cake\ORM\Query $query The original query to modify
365:      * @param array $options Options
366:      * @return \Cake\ORM\Query
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:      * Determine the reference name to use for a given table
386:      *
387:      * The reference name is usually derived from the class name of the table object
388:      * (PostsTable -> Posts), however for autotable instances it is derived from
389:      * the database table the object points at - or as a last resort, the alias
390:      * of the autotable instance.
391:      *
392:      * @param \Cake\ORM\Table $table The table class to get a reference name for.
393:      * @return string
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:      * Modifies the results from a table find in order to merge the translated fields
409:      * into each entity for a given locale.
410:      *
411:      * @param \Cake\Datasource\ResultSetInterface $results Results to map.
412:      * @param string $locale Locale string
413:      * @return \Cake\Collection\Collection
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:      * Modifies the results from a table find in order to merge full translation records
451:      * into each entity under the `_translations` key
452:      *
453:      * @param \Cake\Datasource\ResultSetInterface $results Results to modify.
454:      * @return \Cake\Collection\Collection
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:      * Helper method used to generated multiple translated field entities
490:      * out of the data found in the `_translations` property in the passed
491:      * entity. The result will be put into its `_i18n` property
492:      *
493:      * @param \Cake\Datasource\EntityInterface $entity Entity
494:      * @return void
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:      * Returns the ids found for each of the condition arrays passed for the translations
543:      * table. Each records is indexed by the corresponding position to the conditions array
544:      *
545:      * @param array $ruleSet an array of arary of conditions to be used for finding each
546:      * @return array
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: 
Follow @CakePHP
#IRC
OpenHub
Rackspace
Rackspace
  • Business Solutions
  • Showcase
  • Documentation
  • Book
  • API
  • Videos
  • Logos & Trademarks
  • Community
  • Team
  • Issues (Github)
  • YouTube Channel
  • Get Involved
  • Bakery
  • Featured Resources
  • Newsletter
  • Certification
  • My CakePHP
  • CakeFest
  • Facebook
  • Twitter
  • Help & Support
  • Forum
  • Stack Overflow
  • IRC
  • Slack
  • Paid Support

Generated using CakePHP API Docs