DefaultLocalization.php
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2015 wemove digital solutions GmbH
5  *
6  * Licensed under the terms of the MIT License.
7  *
8  * See the LICENSE file distributed with this work for
9  * additional information.
10  */
11 namespace wcmf\lib\i18n\impl;
12 
26 
27 /**
28  * DefaultLocalization is a Localization implementation that saves translations
29  * in the store. Entity instances are localized value by value, where a
30  * translation of a value of one instance into a specific language
31  * is represented by one instance of the translation entity type (e.g. Translation).
32  *
33  * The translation entity type must have the attributes 'objectid', 'attribute',
34  * 'translation', 'language' with the appropriate getter and setter methods. It
35  * is defined calling the Localization::setTranslationType() method.
36  *
37  * A translation is only stored for values with the tag TRANSLATABLE
38  * (see AttributeDescription). This allows to exclude certain values
39  * e.g. date values from the translation process by omitting this tag.
40  *
41  * The default language is defined in the configuration key 'defaultLanguage'
42  * in section 'localization'.
43  *
44  * All languages available for translation are either defined in the configuration
45  * section 'languages', where each language has it's own entry: e.g. en = English
46  * or in a language entity type (e.g. Language). The language entity type must
47  * have the attributes 'code' and 'name' with the appropriate getter and setter
48  * methods. It is defined calling the Localization::setLanguageType() method.
49  * If translation entity type and configuration section are defined, the
50  * configuration section is preferred.
51  *
52  * @author ingo herwig <ingo@wemove.com>
53  */
55 
56  private $_persistenceFacade = null;
57  private $_configuration = null;
58 
59  private $_supportedLanguages = null;
60  private $_defaultLanguage = null;
61  private $_translationType = null;
62  private $_languageType = null;
63 
64  /**
65  * Configuration
66  * @param $persistenceFacade
67  * @param $configuration
68  * @param $defaultLanguage
69  * @param $translationType Entity type name
70  * @param $languageType Entity type name
71  */
72  public function __construct(PersistenceFacade $persistenceFacade,
73  Configuration $configuration, $defaultLanguage, $translationType, $languageType) {
74  $this->_persistenceFacade = $persistenceFacade;
75  $this->_configuration = $configuration;
76  $supportedLanguages = $this->getSupportedLanguages();
77 
78  if (!isset($supportedLanguages[$defaultLanguage])) {
79  throw new ConfigurationException('No supported language equals the default language \''.$defaultLanguage.'\'');
80  }
81  $this->_defaultLanguage = $defaultLanguage;
82 
83  if (!$this->_persistenceFacade->isKnownType($translationType)) {
84  throw new IllegalArgumentException('The translation type \''.$translationType.'\' is unknown.');
85  }
86  $this->_translationType = $translationType;
87 
88  if (!$this->_persistenceFacade->isKnownType($languageType)) {
89  throw new IllegalArgumentException('The language type \''.$languageType.'\' is unknown.');
90  }
91  $this->_languageType = $languageType;
92  }
93 
94  /**
95  * @see Localization::getDefaultLanguage()
96  */
97  public function getDefaultLanguage() {
98  return $this->_defaultLanguage;
99  }
100 
101  /**
102  * @see Localization::getSupportedLanguages()
103  * Reads the configuration section 'languages'
104  */
105  public function getSupportedLanguages() {
106  if ($this->_supportedLanguages == null) {
107  // check if the configuration section exists
108  if (($languages = $this->_configuration->getSection('languages')) !== false) {
109  $this->_supportedLanguages = $languages;
110  }
111  // if not, use the languageType
112  else {
113  $languages = $this->_persistenceFacade->loadObjects($this->_languageType, BuildDepth::SINGLE);
114  for($i=0, $count=sizeof($languages); $i<$count; $i++) {
115  $curLanguage = $languages[$i];
116  $this->_supportedLanguages[$curLanguage->getCode()] = $curLanguage->getName();
117  }
118  }
119  }
120  return $this->_supportedLanguages;
121  }
122 
123  /**
124  * @see Localization::loadTranslatedObject()
125  */
126  public function loadTranslatedObject(ObjectId $oid, $lang, $useDefaults=true) {
127  $object = $this->_persistenceFacade->load($oid, BuildDepth::SINGLE);
128 
129  return $this->loadTranslation($object, $lang, $useDefaults, false);
130  }
131 
132  /**
133  * @see Localization::loadTranslation()
134  */
135  public function loadTranslation(PersistentObject $object, $lang, $useDefaults=true, $recursive=true) {
136  $translatedObject = $this->loadTranslationImpl($object, $lang, $useDefaults);
137  $object->setProperty(__CLASS__.'.loaded', true);
138 
139  // recurse if requested
140  if ($recursive) {
141  $relations = $object->getMapper()->getRelations('child');
142  foreach ($relations as $relation) {
143  if ($relation->getOtherNavigability()) {
144  $role = $relation->getOtherRole();
145  $childValue = $object->getValue($role);
146  if ($childValue != null) {
147  $children = $relation->isMultiValued() ? $childValue : array($childValue);
148  foreach ($children as $child) {
149  // don't resolve proxies
150  if (!($child instanceof PersistentObjectProxy) && $child->getProperty(__CLASS__.'.loaded') !== true) {
151  $translatedChild = $this->loadTranslation($child, $lang, $useDefaults, $recursive);
152  $translatedObject->addNode($translatedChild, $role);
153  }
154  }
155  }
156  }
157  }
158  }
159  return $translatedObject;
160  }
161 
162  /**
163  * Load a translation of a single entity for a specific language.
164  * @param $object A reference to the object to load the translation into. The object
165  * is supposed to have it's values in the default language.
166  * @param $lang The language of the translation to load.
167  * @param $useDefaults Boolean whether to use the default language values
168  * for untranslated/empty values or not. Optional, default is true.
169  * @return A reference to the translated object.
170  * @throws IllegalArgumentException
171  */
172  protected function loadTranslationImpl(PersistentObject $object, $lang, $useDefaults=true) {
173  if ($object == null) {
174  throw new IllegalArgumentException('Cannot load translation for null');
175  }
176 
177  $translatedObject = $object;
178  $oidStr = $object->getOID()->__toString();
179 
180  // load the translations and translate the object for any language
181  // different to the default language
182  if ($lang != $this->getDefaultLanguage()) {
183  $transaction = $this->_persistenceFacade->getTransaction();
184  $translatedObject = $this->_persistenceFacade->create($object->getType());
185  $transaction->detach($translatedObject->getOID());
186  $object->copyValues($translatedObject, true);
187 
188  $query = new ObjectQuery($this->_translationType, __CLASS__.'load_save');
189  $tpl = $query->getObjectTemplate($this->_translationType);
190  $tpl->setValue('objectid', Criteria::asValue('=', $oidStr));
191  $tpl->setValue('language', Criteria::asValue('=', $lang));
192  $translations = $query->execute(BuildDepth::SINGLE);
193 
194  // set the translated values in the object
195  $iter = new NodeValueIterator($object, false);
196  for($iter->rewind(); $iter->valid(); $iter->next()) {
197  $this->setTranslatedValue($translatedObject, $iter->key(), $translations, $useDefaults);
198  }
199  }
200  return $translatedObject;
201  }
202 
203  /**
204  * @see Localization::loadTranslation()
205  * @note Only values with tag TRANSLATABLE are stored.
206  */
207  public function saveTranslation(PersistentObject $object, $lang, $recursive=true) {
208  $this->saveTranslationImpl($object, $lang);
209 
210  // recurse if requested
211  if ($recursive) {
212  $iterator = new NodeIterator($object);
213  foreach($iterator as $oidStr => $obj) {
214  if ($obj->getOID() != $object->getOID()) {
215  // don't resolve proxies
216  if (!($obj instanceof PersistentObjectProxy)) {
217  $this->saveTranslation($obj, $lang, $recursive);
218  }
219  }
220  }
221  }
222  }
223 
224  /**
225  * Save a translation of a single entity for a specific language. Only the
226  * values that have a non-empty value are considered as translations and stored.
227  * @param $object An instance of the entity type that holds the translations as values.
228  * @param $lang The language of the translation.
229  */
230  protected function saveTranslationImpl(PersistentObject $object, $lang) {
231  // if the requested language is the default language, do nothing
232  if ($lang == $this->getDefaultLanguage()) {
233  // nothing to do
234  }
235  // save the translations for any other language
236  else {
237  $object->beforeUpdate();
238 
239  // get the existing translations for the requested language
240  $query = new ObjectQuery($this->_translationType, __CLASS__.'load_save');
241  $tpl = $query->getObjectTemplate($this->_translationType);
242  $tpl->setValue('objectid', Criteria::asValue('=', $object->getOID()->__toString()));
243  $tpl->setValue('language', Criteria::asValue('=', $lang));
244  $translations = $query->execute(BuildDepth::SINGLE);
245 
246  // save the translations, ignore pk values
247  $pkNames = $object->getMapper()->getPkNames();
248  $iter = new NodeValueIterator($object, false);
249  for($iter->rewind(); $iter->valid(); $iter->next()) {
250  $valueName = $iter->key();
251  if (!in_array($valueName, $pkNames)) {
252  $curIterNode = $iter->currentNode();
253  $this->saveTranslatedValue($curIterNode, $valueName, $translations, $lang);
254  }
255  }
256  }
257  }
258 
259  /**
260  * @see Localization::deleteTranslation()
261  */
262  public function deleteTranslation(ObjectId $oid, $lang=null) {
263  // if the requested language is the default language, do nothing
264  if ($lang == $this->getDefaultLanguage()) {
265  // nothing to do
266  }
267  // delete the translations for any other language
268  else {
269  // get the existing translations for the requested language or all languages
270  $query = new ObjectQuery($this->_translationType, __CLASS__.'delete_trans'.($lang != null));
271  $tpl = $query->getObjectTemplate($this->_translationType);
272  $tpl->setValue('objectid', Criteria::asValue('=', $oid->__toString()));
273  if ($lang != null) {
274  $tpl->setValue('language', Criteria::asValue('=', $lang));
275  }
276  $translations = $query->execute(BuildDepth::SINGLE);
277 
278  // delete the found tranlations
279  foreach ($translations as $curTranslation) {
280  $curTranslation->delete();
281  }
282  }
283  }
284 
285  /**
286  * @see Localization::deleteLanguage()
287  */
288  public function deleteLanguage($lang) {
289  // if the requested language is the default language, do nothing
290  if ($lang == $this->getDefaultLanguage()) {
291  // nothing to do
292  }
293  // delete the translations for any other language
294  else {
295  // get the existing translations for the requested language
296  $query = new ObjectQuery($this->_translationType, __CLASS__.'delete_lang');
297  $tpl = $query->getObjectTemplate($this->_translationType);
298  $tpl->setValue('language', Criteria::asValue('=', $lang));
299  $translations = $query->execute(BuildDepth::SINGLE);
300 
301  // delete the found tranlations
302  foreach ($translations as $curTranslation) {
303  $curTranslation->delete();
304  }
305  }
306  }
307 
308  /**
309  * Set a translated value in the given PersistentObject instance.
310  * @param $object The object to set the value on. The object
311  * is supposed to have it's values in the default language.
312  * @param $valueName The name of the value to translate
313  * @param $translations An array of translation instances for the object.
314  * @param $useDefaults Boolean whether to use the default language if no
315  * translation is found or not.
316  */
317  private function setTranslatedValue(PersistentObject $object, $valueName, array $translations, $useDefaults) {
318  $mapper = $object->getMapper();
319  $isTranslatable = $mapper != null && $mapper->hasAttribute($valueName) ? $mapper->getAttribute($valueName)->hasTag('TRANSLATABLE') : false;
320  if ($isTranslatable) {
321  // empty the value, if the default language values should not be used
322  if (!$useDefaults) {
323  $object->setValue($valueName, null, true);
324  }
325  // translate the value
326  for ($i=0, $count=sizeof($translations); $i<$count; $i++) {
327  $curValueName = $translations[$i]->getValue('attribute');
328  if ($curValueName == $valueName) {
329  $translation = $translations[$i]->getValue('translation');
330  if (!($useDefaults && strlen($translation) == 0)) {
331  $object->setValue($valueName, $translation, true);
332  }
333  break;
334  }
335  }
336  }
337  }
338 
339  /**
340  * Save translated values for the given object
341  * @param $object The object to save the translations on
342  * @param $valueName The name of the value to translate
343  * @param $existingTranslations An array of already existing translation
344  * instances for the object.
345  * @param $lang The language of the translations.
346  */
347  private function saveTranslatedValue(PersistentObject $object, $valueName, array $existingTranslations, $lang) {
348  $mapper = $object->getMapper();
349  $isTranslatable = $mapper != null && $mapper->hasAttribute($valueName) ? $mapper->getAttribute($valueName)->hasTag('TRANSLATABLE') : false;
350  if ($isTranslatable) {
351  $value = $object->getValue($valueName);
352  $translation = null;
353 
354  // check if a translation already exists
355  for ($i=0, $count=sizeof($existingTranslations); $i<$count; $i++) {
356  $curValueName = $existingTranslations[$i]->getValue('attribute');
357  if ($curValueName == $valueName) {
358  $translation = &$existingTranslations[$i];
359  break;
360  }
361  }
362 
363  // if not, create a new translation
364  if ($translation == null) {
365  $translation = $this->_persistenceFacade->create($this->_translationType);
366  }
367 
368  // set all required properties
369  $translation->setValue('objectid', $object->getOID()->__toString());
370  $translation->setValue('attribute', $valueName);
371  $translation->setValue('translation', $object->getValue($valueName));
372  $translation->setValue('language', $lang);
373  }
374  }
375 }
376 ?>
getType()
Get the type of the object.
Localization defines the interface for storing localized entity instances and retrieving them back...
getOID()
Get the object id of the PersistentObject.
saveTranslation(PersistentObject $object, $lang, $recursive=true)
beforeUpdate()
This method is called always before updating the modified object in the store.
IllegalArgumentException signals an exception in method arguments.
loadTranslationImpl(PersistentObject $object, $lang, $useDefaults=true)
Load a translation of a single entity for a specific language.
NodeValueIterator is used to iterate over all persistent values of a Node (not including relations)...
getMapper()
Get the PersistenceMapper of the object.
ObjectQuery implements a template based object query.
NodeIterator is used to iterate over a tree/list built of Nodes using a Depth-First-Algorithm.
ObjectId is the unique identifier of an object.
Definition: ObjectId.php:27
DefaultLocalization is a Localization implementation that saves translations in the store...
PersistentObjectProxy is proxy for an PersistentObject instance.
Implementations of Configuration give access to the application configuration.
deleteTranslation(ObjectId $oid, $lang=null)
__toString()
Get a string representation of the object id.
Definition: ObjectId.php:204
loadTranslation(PersistentObject $object, $lang, $useDefaults=true, $recursive=true)
setValue($name, $value, $forceSet=false, $trackChange=true)
Set the value of a named item if it exists.
copyValues(PersistentObject $object, $copyPkValues=true)
Copy all non-empty values to a given instance (ChangeListeners are triggered)
PersistenceFacade defines the interface for PersistenceFacade implementations.
__construct(PersistenceFacade $persistenceFacade, Configuration $configuration, $defaultLanguage, $translationType, $languageType)
Configuration.
saveTranslationImpl(PersistentObject $object, $lang)
Save a translation of a single entity for a specific language.
ConfigurationException signals an exception in the configuration.
PersistentObject defines the interface of all persistent objects.