1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15:
16:
17: App::uses('ModelBehavior', 'Model');
18: App::uses('I18n', 'I18n');
19: App::uses('I18nModel', 'Model');
20:
21: 22: 23: 24: 25: 26:
27: class TranslateBehavior extends ModelBehavior {
28:
29: 30: 31: 32: 33:
34: public $runtime = array();
35:
36: 37: 38: 39: 40:
41: protected $_joinTable;
42:
43: 44: 45: 46: 47:
48: protected $_runtimeModel;
49:
50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66:
67: public function setup(Model $Model, $config = array()) {
68: $db = ConnectionManager::getDataSource($Model->useDbConfig);
69: if (!$db->connected) {
70: trigger_error(
71: __d('cake_dev', 'Datasource %s for TranslateBehavior of model %s is not connected', $Model->useDbConfig, $Model->alias),
72: E_USER_ERROR
73: );
74: return false;
75: }
76:
77: $this->settings[$Model->alias] = array();
78: $this->runtime[$Model->alias] = array('fields' => array());
79: $this->translateModel($Model);
80: return $this->bindTranslation($Model, $config, false);
81: }
82:
83: 84: 85: 86: 87: 88:
89: public function cleanup(Model $Model) {
90: $this->unbindTranslation($Model);
91: unset($this->settings[$Model->alias]);
92: unset($this->runtime[$Model->alias]);
93: }
94:
95: 96: 97: 98: 99: 100: 101:
102: public function beforeFind(Model $Model, $query) {
103: $this->runtime[$Model->alias]['virtualFields'] = $Model->virtualFields;
104: $locale = $this->_getLocale($Model);
105: if (empty($locale)) {
106: return $query;
107: }
108: $db = $Model->getDataSource();
109: $RuntimeModel = $this->translateModel($Model);
110:
111: if (!empty($RuntimeModel->tablePrefix)) {
112: $tablePrefix = $RuntimeModel->tablePrefix;
113: } else {
114: $tablePrefix = $db->config['prefix'];
115: }
116: $joinTable = new StdClass();
117: $joinTable->tablePrefix = $tablePrefix;
118: $joinTable->table = $RuntimeModel->table;
119: $joinTable->schemaName = $RuntimeModel->getDataSource()->getSchemaName();
120:
121: $this->_joinTable = $joinTable;
122: $this->_runtimeModel = $RuntimeModel;
123:
124: if (is_string($query['fields']) && $query['fields'] === "COUNT(*) AS {$db->name('count')}") {
125: $query['fields'] = "COUNT(DISTINCT({$db->name($Model->escapeField())})) {$db->alias}count";
126: $query['joins'][] = array(
127: 'type' => 'INNER',
128: 'alias' => $RuntimeModel->alias,
129: 'table' => $joinTable,
130: 'conditions' => array(
131: $Model->escapeField() => $db->identifier($RuntimeModel->escapeField('foreign_key')),
132: $RuntimeModel->escapeField('model') => $Model->name,
133: $RuntimeModel->escapeField('locale') => $locale
134: )
135: );
136: $conditionFields = $this->_checkConditions($Model, $query);
137: foreach ($conditionFields as $field) {
138: $query = $this->_addJoin($Model, $query, $field, $field, $locale);
139: }
140: unset($this->_joinTable, $this->_runtimeModel);
141: return $query;
142: } elseif (is_string($query['fields'])) {
143: $query['fields'] = CakeText::tokenize($query['fields']);
144: }
145:
146: $fields = array_merge(
147: $this->settings[$Model->alias],
148: $this->runtime[$Model->alias]['fields']
149: );
150: $addFields = array();
151: if (empty($query['fields'])) {
152: $addFields = $fields;
153: } elseif (is_array($query['fields'])) {
154: $isAllFields = (
155: in_array($Model->alias . '.' . '*', $query['fields']) ||
156: in_array($Model->escapeField('*'), $query['fields'])
157: );
158: foreach ($fields as $key => $value) {
159: $field = (is_numeric($key)) ? $value : $key;
160: if ($isAllFields ||
161: in_array($Model->alias . '.' . $field, $query['fields']) ||
162: in_array($field, $query['fields'])
163: ) {
164: $addFields[] = $field;
165: }
166: }
167: }
168:
169: $this->runtime[$Model->alias]['virtualFields'] = $Model->virtualFields;
170: if ($addFields) {
171: foreach ($addFields as $_f => $field) {
172: $aliasField = is_numeric($_f) ? $field : $_f;
173:
174: foreach (array($aliasField, $Model->alias . '.' . $aliasField) as $_field) {
175: $key = array_search($_field, (array)$query['fields']);
176:
177: if ($key !== false) {
178: unset($query['fields'][$key]);
179: }
180: }
181: $query = $this->_addJoin($Model, $query, $field, $aliasField, $locale);
182: }
183: }
184: $this->runtime[$Model->alias]['beforeFind'] = $addFields;
185: unset($this->_joinTable, $this->_runtimeModel);
186: return $query;
187: }
188:
189: 190: 191: 192: 193: 194: 195: 196:
197: protected function _checkConditions(Model $Model, $query) {
198: $conditionFields = array();
199: if (empty($query['conditions']) || (!empty($query['conditions']) && !is_array($query['conditions']))) {
200: return $conditionFields;
201: }
202: foreach ($query['conditions'] as $col => $val) {
203: foreach ($this->settings[$Model->alias] as $field => $assoc) {
204: if (is_numeric($field)) {
205: $field = $assoc;
206: }
207: if (strpos($col, $field) !== false) {
208: $conditionFields[] = $field;
209: }
210: }
211: }
212: return $conditionFields;
213: }
214:
215: 216: 217: 218: 219: 220: 221: 222: 223: 224:
225: protected function _addJoin(Model $Model, $query, $field, $aliasField, $locale) {
226: $db = ConnectionManager::getDataSource($Model->useDbConfig);
227: $RuntimeModel = $this->_runtimeModel;
228: $joinTable = $this->_joinTable;
229: $aliasVirtual = "i18n_{$field}";
230: $alias = "I18n__{$field}";
231: if (is_array($locale)) {
232: foreach ($locale as $_locale) {
233: $aliasVirtualLocale = "{$aliasVirtual}_{$_locale}";
234: $aliasLocale = "{$alias}__{$_locale}";
235: $Model->virtualFields[$aliasVirtualLocale] = "{$aliasLocale}.content";
236: if (!empty($query['fields']) && is_array($query['fields'])) {
237: $query['fields'][] = $aliasVirtualLocale;
238: }
239: $query['joins'][] = array(
240: 'type' => 'LEFT',
241: 'alias' => $aliasLocale,
242: 'table' => $joinTable,
243: 'conditions' => array(
244: $Model->escapeField() => $db->identifier("{$aliasLocale}.foreign_key"),
245: "{$aliasLocale}.model" => $Model->name,
246: "{$aliasLocale}.{$RuntimeModel->displayField}" => $aliasField,
247: "{$aliasLocale}.locale" => $_locale
248: )
249: );
250: }
251: } else {
252: $Model->virtualFields[$aliasVirtual] = "{$alias}.content";
253: if (!empty($query['fields']) && is_array($query['fields'])) {
254: $query['fields'][] = $aliasVirtual;
255: }
256: $query['joins'][] = array(
257: 'type' => 'INNER',
258: 'alias' => $alias,
259: 'table' => $joinTable,
260: 'conditions' => array(
261: "{$Model->alias}.{$Model->primaryKey}" => $db->identifier("{$alias}.foreign_key"),
262: "{$alias}.model" => $Model->name,
263: "{$alias}.{$RuntimeModel->displayField}" => $aliasField,
264: "{$alias}.locale" => $locale
265: )
266: );
267: }
268: return $query;
269: }
270:
271: 272: 273: 274: 275: 276: 277: 278:
279: public function afterFind(Model $Model, $results, $primary = false) {
280: $Model->virtualFields = $this->runtime[$Model->alias]['virtualFields'];
281:
282: $this->runtime[$Model->alias]['virtualFields'] = $this->runtime[$Model->alias]['fields'] = array();
283: if (!empty($this->runtime[$Model->alias]['restoreFields'])) {
284: $this->runtime[$Model->alias]['fields'] = $this->runtime[$Model->alias]['restoreFields'];
285: unset($this->runtime[$Model->alias]['restoreFields']);
286: }
287:
288: $locale = $this->_getLocale($Model);
289:
290: if (empty($locale) || empty($results) || empty($this->runtime[$Model->alias]['beforeFind'])) {
291: return $results;
292: }
293: $beforeFind = $this->runtime[$Model->alias]['beforeFind'];
294:
295: foreach ($results as $key => &$row) {
296: $results[$key][$Model->alias]['locale'] = (is_array($locale)) ? current($locale) : $locale;
297: foreach ($beforeFind as $_f => $field) {
298: $aliasField = is_numeric($_f) ? $field : $_f;
299: $aliasVirtual = "i18n_{$field}";
300: if (is_array($locale)) {
301: foreach ($locale as $_locale) {
302: $aliasVirtualLocale = "{$aliasVirtual}_{$_locale}";
303: if (!isset($row[$Model->alias][$aliasField]) && !empty($row[$Model->alias][$aliasVirtualLocale])) {
304: $row[$Model->alias][$aliasField] = $row[$Model->alias][$aliasVirtualLocale];
305: $row[$Model->alias]['locale'] = $_locale;
306: }
307: unset($row[$Model->alias][$aliasVirtualLocale]);
308: }
309:
310: if (!isset($row[$Model->alias][$aliasField])) {
311: $row[$Model->alias][$aliasField] = '';
312: }
313: } else {
314: $value = '';
315: if (isset($row[$Model->alias][$aliasVirtual])) {
316: $value = $row[$Model->alias][$aliasVirtual];
317: }
318: $row[$Model->alias][$aliasField] = $value;
319: unset($row[$Model->alias][$aliasVirtual]);
320: }
321: }
322: }
323: return $results;
324: }
325:
326: 327: 328: 329: 330: 331: 332: 333:
334: public function beforeValidate(Model $Model, $options = array()) {
335: unset($this->runtime[$Model->alias]['beforeSave']);
336: $this->_setRuntimeData($Model);
337: return true;
338: }
339:
340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350:
351: public function beforeSave(Model $Model, $options = array()) {
352: if (isset($options['validate']) && !$options['validate']) {
353: unset($this->runtime[$Model->alias]['beforeSave']);
354: }
355: if (isset($this->runtime[$Model->alias]['beforeSave'])) {
356: return true;
357: }
358: $this->_setRuntimeData($Model);
359: return true;
360: }
361:
362: 363: 364: 365: 366: 367: 368: 369: 370: 371:
372: protected function _setRuntimeData(Model $Model) {
373: $locale = $this->_getLocale($Model);
374: if (empty($locale)) {
375: return true;
376: }
377: $fields = array_merge($this->settings[$Model->alias], $this->runtime[$Model->alias]['fields']);
378: $tempData = array();
379:
380: foreach ($fields as $key => $value) {
381: $field = (is_numeric($key)) ? $value : $key;
382:
383: if (isset($Model->data[$Model->alias][$field])) {
384: $tempData[$field] = $Model->data[$Model->alias][$field];
385: if (is_array($Model->data[$Model->alias][$field])) {
386: if (is_string($locale) && !empty($Model->data[$Model->alias][$field][$locale])) {
387: $Model->data[$Model->alias][$field] = $Model->data[$Model->alias][$field][$locale];
388: } else {
389: $values = array_values($Model->data[$Model->alias][$field]);
390: $Model->data[$Model->alias][$field] = $values[0];
391: }
392: }
393: }
394: }
395: $this->runtime[$Model->alias]['beforeSave'] = $tempData;
396: }
397:
398: 399: 400: 401: 402: 403: 404:
405: public function afterValidate(Model $Model) {
406: $Model->data[$Model->alias] = array_merge(
407: $Model->data[$Model->alias],
408: $this->runtime[$Model->alias]['beforeSave']
409: );
410: return true;
411: }
412:
413: 414: 415: 416: 417: 418: 419: 420:
421: public function afterSave(Model $Model, $created, $options = array()) {
422: if (!isset($this->runtime[$Model->alias]['beforeValidate']) && !isset($this->runtime[$Model->alias]['beforeSave'])) {
423: return true;
424: }
425: if (isset($this->runtime[$Model->alias]['beforeValidate'])) {
426: $tempData = $this->runtime[$Model->alias]['beforeValidate'];
427: } else {
428: $tempData = $this->runtime[$Model->alias]['beforeSave'];
429: }
430:
431: unset($this->runtime[$Model->alias]['beforeValidate'], $this->runtime[$Model->alias]['beforeSave']);
432: $conditions = array('model' => $Model->name, 'foreign_key' => $Model->id);
433: $RuntimeModel = $this->translateModel($Model);
434:
435: if ($created) {
436: $tempData = $this->_prepareTranslations($Model, $tempData);
437: }
438: $locale = $this->_getLocale($Model);
439: $atomic = array();
440: if (isset($options['atomic'])) {
441: $atomic = array('atomic' => $options['atomic']);
442: }
443:
444: foreach ($tempData as $field => $value) {
445: unset($conditions['content']);
446: $conditions['field'] = $field;
447: if (is_array($value)) {
448: $conditions['locale'] = array_keys($value);
449: } else {
450: $conditions['locale'] = $locale;
451: if (is_array($locale)) {
452: $value = array($locale[0] => $value);
453: } else {
454: $value = array($locale => $value);
455: }
456: }
457: $translations = $RuntimeModel->find('list', array(
458: 'conditions' => $conditions,
459: 'fields' => array(
460: $RuntimeModel->alias . '.locale',
461: $RuntimeModel->alias . '.id'
462: )
463: ));
464: foreach ($value as $_locale => $_value) {
465: $RuntimeModel->create();
466: $conditions['locale'] = $_locale;
467: $conditions['content'] = $_value;
468: if (array_key_exists($_locale, $translations)) {
469: $RuntimeModel->save(array(
470: $RuntimeModel->alias => array_merge(
471: $conditions, array('id' => $translations[$_locale])
472: ),
473: $atomic
474: ));
475: } else {
476: $RuntimeModel->save(array($RuntimeModel->alias => $conditions), $atomic);
477: }
478: }
479: }
480: }
481:
482: 483: 484: 485: 486: 487: 488: 489:
490: protected function _prepareTranslations(Model $Model, $data) {
491: $fields = array_merge($this->settings[$Model->alias], $this->runtime[$Model->alias]['fields']);
492: $locales = array();
493: foreach ($data as $key => $value) {
494: if (is_array($value)) {
495: $locales = array_merge($locales, array_keys($value));
496: }
497: }
498: $locales = array_unique($locales);
499: $hasLocales = count($locales) > 0;
500:
501: foreach ($fields as $key => $field) {
502: if (!is_numeric($key)) {
503: $field = $key;
504: }
505: if ($hasLocales && !isset($data[$field])) {
506: $data[$field] = array_fill_keys($locales, '');
507: } elseif (!isset($data[$field])) {
508: $data[$field] = '';
509: }
510: }
511: return $data;
512: }
513:
514: 515: 516: 517: 518: 519:
520: public function afterDelete(Model $Model) {
521: $RuntimeModel = $this->translateModel($Model);
522: $conditions = array('model' => $Model->name, 'foreign_key' => $Model->id);
523: $RuntimeModel->deleteAll($conditions);
524: }
525:
526: 527: 528: 529: 530: 531:
532: protected function _getLocale(Model $Model) {
533: if (!isset($Model->locale) || $Model->locale === null) {
534: $I18n = I18n::getInstance();
535: $I18n->l10n->get(Configure::read('Config.language'));
536: $Model->locale = $I18n->l10n->locale;
537: }
538:
539: return $Model->locale;
540: }
541:
542: 543: 544: 545: 546: 547: 548: 549: 550:
551: public function translateModel(Model $Model) {
552: if (!isset($this->runtime[$Model->alias]['model'])) {
553: if (!isset($Model->translateModel) || empty($Model->translateModel)) {
554: $className = 'I18nModel';
555: } else {
556: $className = $Model->translateModel;
557: }
558:
559: $this->runtime[$Model->alias]['model'] = ClassRegistry::init($className);
560: }
561: if (!empty($Model->translateTable) && $Model->translateTable !== $this->runtime[$Model->alias]['model']->useTable) {
562: $this->runtime[$Model->alias]['model']->setSource($Model->translateTable);
563: } elseif (empty($Model->translateTable) && empty($Model->translateModel)) {
564: $this->runtime[$Model->alias]['model']->setSource('i18n');
565: }
566: return $this->runtime[$Model->alias]['model'];
567: }
568:
569: 570: 571: 572: 573: 574: 575: 576: 577: 578: 579: 580: 581: 582: 583:
584: public function bindTranslation(Model $Model, $fields, $reset = true) {
585: if (is_string($fields)) {
586: $fields = array($fields);
587: }
588: $associations = array();
589: $RuntimeModel = $this->translateModel($Model);
590: $default = array(
591: 'className' => $RuntimeModel->alias,
592: 'foreignKey' => 'foreign_key',
593: 'order' => 'id'
594: );
595:
596: foreach ($fields as $key => $value) {
597: if (is_numeric($key)) {
598: $field = $value;
599: $association = null;
600: } else {
601: $field = $key;
602: $association = $value;
603: }
604: if ($association === 'name') {
605: throw new CakeException(
606: __d('cake_dev', 'You cannot bind a translation named "name".')
607: );
608: }
609: $this->_removeField($Model, $field);
610:
611: if ($association === null) {
612: if ($reset) {
613: $this->runtime[$Model->alias]['fields'][] = $field;
614: } else {
615: $this->settings[$Model->alias][] = $field;
616: }
617: } else {
618: if ($reset) {
619: $this->runtime[$Model->alias]['fields'][$field] = $association;
620: $this->runtime[$Model->alias]['restoreFields'][] = $field;
621: } else {
622: $this->settings[$Model->alias][$field] = $association;
623: }
624:
625: foreach (array('hasOne', 'hasMany', 'belongsTo', 'hasAndBelongsToMany') as $type) {
626: if (isset($Model->{$type}[$association]) || isset($Model->__backAssociation[$type][$association])) {
627: trigger_error(
628: __d('cake_dev', 'Association %s is already bound to model %s', $association, $Model->alias),
629: E_USER_ERROR
630: );
631: return false;
632: }
633: }
634: $associations[$association] = array_merge($default, array('conditions' => array(
635: 'model' => $Model->name,
636: $RuntimeModel->displayField => $field
637: )));
638: }
639: }
640:
641: if (!empty($associations)) {
642: $Model->bindModel(array('hasMany' => $associations), $reset);
643: }
644: return true;
645: }
646:
647: 648: 649: 650: 651: 652: 653:
654: protected function _removeField(Model $Model, $field) {
655: if (array_key_exists($field, $this->settings[$Model->alias])) {
656: unset($this->settings[$Model->alias][$field]);
657: } elseif (in_array($field, $this->settings[$Model->alias])) {
658: $this->settings[$Model->alias] = array_merge(array_diff($this->settings[$Model->alias], array($field)));
659: }
660:
661: if (array_key_exists($field, $this->runtime[$Model->alias]['fields'])) {
662: unset($this->runtime[$Model->alias]['fields'][$field]);
663: } elseif (in_array($field, $this->runtime[$Model->alias]['fields'])) {
664: $this->runtime[$Model->alias]['fields'] = array_merge(array_diff($this->runtime[$Model->alias]['fields'], array($field)));
665: }
666: }
667:
668: 669: 670: 671: 672: 673: 674: 675: 676:
677: public function unbindTranslation(Model $Model, $fields = null) {
678: if (empty($fields) && empty($this->settings[$Model->alias])) {
679: return false;
680: }
681: if (empty($fields)) {
682: return $this->unbindTranslation($Model, $this->settings[$Model->alias]);
683: }
684:
685: if (is_string($fields)) {
686: $fields = array($fields);
687: }
688: $associations = array();
689:
690: foreach ($fields as $key => $value) {
691: if (is_numeric($key)) {
692: $field = $value;
693: $association = null;
694: } else {
695: $field = $key;
696: $association = $value;
697: }
698:
699: $this->_removeField($Model, $field);
700:
701: if ($association !== null && (isset($Model->hasMany[$association]) || isset($Model->__backAssociation['hasMany'][$association]))) {
702: $associations[] = $association;
703: }
704: }
705:
706: if (!empty($associations)) {
707: $Model->unbindModel(array('hasMany' => $associations), false);
708: }
709: return true;
710: }
711:
712: }
713: