DefaultLocalization.php
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2020 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 
30 
31 /**
32  * DefaultLocalization is a Localization implementation that saves translations
33  * in the store. Entity instances are localized value by value, where a
34  * translation of a value of one instance into a specific language
35  * is represented by one instance of the translation entity type (e.g. Translation).
36  *
37  * The translation entity type must have the attributes 'objectid', 'attribute',
38  * 'translation', 'language' with the appropriate getter and setter methods. It
39  * is defined calling the Localization::setTranslationType() method.
40  *
41  * A translation is only stored for values with the tag TRANSLATABLE
42  * (see AttributeDescription). This allows to exclude certain values
43  * e.g. date values from the translation process by omitting this tag.
44  *
45  * The default language is defined in the configuration key 'defaultLanguage'
46  * in section 'localization'.
47  *
48  * All languages available for translation are either defined in the configuration
49  * section 'languages', where each language has it's own entry: e.g. en = English
50  * or in a language entity type (e.g. Language). The language entity type must
51  * have the attributes 'code' and 'name' with the appropriate getter and setter
52  * methods. It is defined calling the Localization::setLanguageType() method.
53  * If translation entity type and configuration section are defined, the
54  * configuration section is preferred.
55  *
56  * @author ingo herwig <ingo@wemove.com>
57  */
59 
60  private $persistenceFacade = null;
61  private $configuration = null;
62  private $eventManager = null;
63 
64  private $supportedLanguages = null;
65  private $defaultLanguage = null;
66  private $translationType = null;
67  private $languageType = null;
68 
69  private $translatedObjects = [];
70  private $createdTranslations = [];
71 
72  private static $isDebugEnabled = false;
73  private static $logger = null;
74 
75  /**
76  * Configuration
77  * @param $persistenceFacade
78  * @param $configuration
79  * @param $eventManager
80  * @param $defaultLanguage
81  * @param $translationType Entity type name
82  * @param $languageType Entity type name
83  */
84  public function __construct(PersistenceFacade $persistenceFacade,
85  Configuration $configuration, EventManager $eventManager,
86  $defaultLanguage, $translationType, $languageType) {
87  if (self::$logger == null) {
88  self::$logger = LogManager::getLogger(__CLASS__);
89  }
90  self::$isDebugEnabled = self::$logger->isDebugEnabled();
91 
92  $this->persistenceFacade = $persistenceFacade;
93  $this->configuration = $configuration;
94  $this->eventManager = $eventManager;
95  $supportedLanguages = $this->getSupportedLanguages();
96 
97  if (!isset($supportedLanguages[$defaultLanguage])) {
98  throw new ConfigurationException('No supported language equals the default language \''.$defaultLanguage.'\'');
99  }
100  $this->defaultLanguage = $defaultLanguage;
101 
102  if (!$this->persistenceFacade->isKnownType($translationType)) {
103  throw new IllegalArgumentException('The translation type \''.$translationType.'\' is unknown.');
104  }
105  $this->translationType = $translationType;
106 
107  if (!$this->persistenceFacade->isKnownType($languageType)) {
108  throw new IllegalArgumentException('The language type \''.$languageType.'\' is unknown.');
109  }
110  $this->languageType = $languageType;
111  $this->eventManager->addListener(PersistenceEvent::NAME, [$this, 'afterCreate']);
112  }
113 
114  /**
115  * Destructor
116  */
117  public function __destruct() {
118  $this->eventManager->removeListener(PersistenceEvent::NAME, [$this, 'afterCreate']);
119  }
120 
121  /**
122  * @see Localization::getDefaultLanguage()
123  */
124  public function getDefaultLanguage() {
125  return $this->defaultLanguage;
126  }
127 
128  /**
129  * @see Localization::getSupportedLanguages()
130  * Reads the configuration section 'languages'
131  */
132  public function getSupportedLanguages() {
133  if ($this->supportedLanguages == null) {
134  // check if the configuration section exists
135  if (($languages = $this->configuration->getSection('languages')) !== false) {
136  $this->supportedLanguages = $languages;
137  }
138  // if not, use the languageType
139  else {
140  $languages = $this->persistenceFacade->loadObjects($this->languageType, BuildDepth::SINGLE);
141  for($i=0, $count=sizeof($languages); $i<$count; $i++) {
142  $curLanguage = $languages[$i];
143  $this->supportedLanguages[$curLanguage->getCode()] = $curLanguage->getName();
144  }
145  }
146  }
147  return $this->supportedLanguages;
148  }
149 
150  /**
151  * @see Localization::loadTranslatedObject()
152  */
153  public function loadTranslatedObject(ObjectId $oid, $lang, $useDefaults=true) {
154  $object = $this->persistenceFacade->load($oid, BuildDepth::SINGLE);
155 
156  return $object != null ? $this->loadTranslation($object, $lang, $useDefaults, false) : null;
157  }
158 
159  /**
160  * @see Localization::loadTranslation()
161  */
162  public function loadTranslation(PersistentObject $object, $lang, $useDefaults=true, $recursive=true, $marker=null) {
163  if (self::$isDebugEnabled) {
164  self::$logger->debug(($marker != null ? strstr($marker, '@').': ' : '')."Load translation [".$lang."] for: ".$object->getOID());
165  }
166  $translatedObject = $this->loadTranslationImpl($object, $lang, $useDefaults);
167 
168  // mark already translated objects to avoid infinite recursion (the marker value is based on the initial object)
169  $marker = $marker == null ? __CLASS__.'@'.$lang.':'.$object->getOID() : $marker;
170  $object->setProperty($marker, true);
171 
172  // recurse if requested
173  if ($recursive) {
174  $relations = $object->getMapper()->getRelations();
175  foreach ($relations as $relation) {
176  if ($relation->getOtherNavigability()) {
177  $role = $relation->getOtherRole();
178  $relationValue = $object->getValue($role);
179  if ($relationValue != null) {
180  $isMultivalued = $relation->isMultiValued();
181  $relatives = $isMultivalued ? $relationValue : [$relationValue];
182  foreach ($relatives as $relative) {
183  if (self::$isDebugEnabled) {
184  self::$logger->debug(($marker != null ? strstr($marker, '@').': ' : '')."Process relative: ".$relative->getOID());
185  }
186  // skip proxies
187  if (!$relative instanceof PersistentObjectProxy) {
188  $translatedRelative = $relative->getProperty($marker) !== true ?
189  $this->loadTranslation($relative, $lang, $useDefaults, $recursive, $marker) :
190  $this->loadTranslationImpl($relative, $lang, $useDefaults);
191  if (self::$isDebugEnabled) {
192  self::$logger->debug(($marker != null ? strstr($marker, '@').': ' : '')."Add relative: ".$relative->getOID());
193  }
194  $translatedObject->deleteNode($relative, $role);
195  $translatedObject->addNode($translatedRelative, $role);
196  }
197  else {
198  if (self::$isDebugEnabled) {
199  self::$logger->debug(($marker != null ? strstr($marker, '@').': ' : '')."Skip proxy relative: ".$relative->getOID());
200  }
201  }
202  }
203  }
204  }
205  }
206  }
207  return $translatedObject;
208  }
209 
210  /**
211  * Load a translation of a single entity for a specific language.
212  * @param $object PersistentObject instance to load the translation into. The object
213  * is supposed to have it's values in the default language.
214  * @param $lang The language of the translation to load.
215  * @param $useDefaults Boolean whether to use the default language values
216  * for untranslated/empty values or not. Optional, default is true.
217  * @return PersistentObject instance
218  * @throws IllegalArgumentException
219  */
220  protected function loadTranslationImpl(PersistentObject $object, $lang, $useDefaults=true) {
221  if ($object == null) {
222  throw new IllegalArgumentException('Cannot load translation for null');
223  }
224 
225  $oidStr = $object->getOID()->__toString();
226 
227  $cacheKey = $oidStr.'.'.$lang.'.'.$useDefaults;
228  if (!isset($this->translatedObjects[$cacheKey])) {
229  $translatedObject = $object;
230 
231  // load the translations and translate the object for any language
232  // different to the default language
233  // NOTE: the original object will be detached from the transaction
234  if ($lang != $this->getDefaultLanguage()) {
235  $transaction = $this->persistenceFacade->getTransaction();
236  $transaction->detach($translatedObject->getOID());
237  $translatedObject = clone $object;
238 
239  $query = new ObjectQuery($this->translationType, __CLASS__.'load_save');
240  $tpl = $query->getObjectTemplate($this->translationType);
241  $tpl->setValue('objectid', Criteria::asValue('=', $oidStr));
242  $tpl->setValue('language', Criteria::asValue('=', $lang));
243  $translationInstances = $query->execute(BuildDepth::SINGLE);
244 
245  // create map for faster access
246  $translations = [];
247  foreach ($translationInstances as $translationInstance) {
248  $translations[$translationInstance->getValue('attribute')] = $translationInstance->getValue('translation');
249  }
250 
251  // set the translated values in the object
252  $iter = new NodeValueIterator($object, false);
253  for($iter->rewind(); $iter->valid(); $iter->next()) {
254  $valueName = $iter->key();
255  $translation = isset($translations[$valueName]) ? $translations[$valueName] : '';
256  $this->setTranslatedValue($translatedObject, $valueName, $translation, $useDefaults);
257  }
258  }
259  $this->translatedObjects[$cacheKey] = $translatedObject;
260  }
261  return $this->translatedObjects[$cacheKey];
262  }
263 
264  /**
265  * @see Localization::loadTranslation()
266  * @note Only values with tag TRANSLATABLE are stored.
267  */
268  public function saveTranslation(PersistentObject $object, $lang, $recursive=true) {
269  $this->saveTranslationImpl($object, $lang);
270 
271  // recurse if requested
272  if ($recursive) {
273  $iterator = new NodeIterator($object);
274  foreach($iterator as $oidStr => $obj) {
275  if ($obj->getOID() != $object->getOID()) {
276  // don't resolve proxies
277  if (!($obj instanceof PersistentObjectProxy)) {
278  $this->saveTranslation($obj, $lang, $recursive);
279  }
280  }
281  }
282  }
283  }
284 
285  /**
286  * Save a translation of a single entity for a specific language. Only the
287  * values that have a non-empty value are considered as translations and stored.
288  * @param $object An instance of the entity type that holds the translations as values.
289  * @param $lang The language of the translation.
290  */
291  protected function saveTranslationImpl(PersistentObject $object, $lang) {
292  // if the requested language is the default language, do nothing
293  if ($lang == $this->getDefaultLanguage()) {
294  // nothing to do
295  }
296  // save the translations for any other language
297  else {
298  $object->beforeUpdate();
299 
300  // get the existing translations for the requested language
301  $query = new ObjectQuery($this->translationType, __CLASS__.'load_save');
302  $tpl = $query->getObjectTemplate($this->translationType);
303  $tpl->setValue('objectid', Criteria::asValue('=', $object->getOID()->__toString()));
304  $tpl->setValue('language', Criteria::asValue('=', $lang));
305  $translationInstances = $query->execute(BuildDepth::SINGLE);
306 
307  // create map for faster access
308  $translations = [];
309  foreach ($translationInstances as $translationInstance) {
310  $translations[$translationInstance->getValue('attribute')] = $translationInstance;
311  }
312 
313  // save the translations, ignore pk values
314  $pkNames = $object->getMapper()->getPkNames();
315  $iter = new NodeValueIterator($object, false);
316  for($iter->rewind(); $iter->valid(); $iter->next()) {
317  $valueName = $iter->key();
318  if (!in_array($valueName, $pkNames)) {
319  $curIterNode = $iter->currentNode();
320  $translation = isset($translations[$valueName]) ? $translations[$valueName] : null;
321  $this->saveTranslatedValue($curIterNode, $valueName, $translation, $lang);
322  }
323  }
324  }
325  }
326 
327  /**
328  * @see Localization::deleteTranslation()
329  */
330  public function deleteTranslation(ObjectId $oid, $lang=null) {
331  // if the requested language is the default language, do nothing
332  if ($lang == $this->getDefaultLanguage()) {
333  // nothing to do
334  }
335  // delete the translations for any other language
336  else {
337  // get the existing translations for the requested language or all languages
338  $query = new ObjectQuery($this->translationType, __CLASS__.'delete_trans'.($lang != null));
339  $tpl = $query->getObjectTemplate($this->translationType);
340  $tpl->setValue('objectid', Criteria::asValue('=', $oid->__toString()));
341  if ($lang != null) {
342  $tpl->setValue('language', Criteria::asValue('=', $lang));
343  }
344  $translations = $query->execute(BuildDepth::SINGLE);
345 
346  // delete the found tranlations
347  foreach ($translations as $curTranslation) {
348  $curTranslation->delete();
349  }
350  }
351  }
352 
353  /**
354  * @see Localization::deleteLanguage()
355  */
356  public function deleteLanguage($lang) {
357  // if the requested language is the default language, do nothing
358  if ($lang == $this->getDefaultLanguage()) {
359  // nothing to do
360  }
361  // delete the translations for any other language
362  else {
363  // get the existing translations for the requested language
364  $query = new ObjectQuery($this->translationType, __CLASS__.'delete_lang');
365  $tpl = $query->getObjectTemplate($this->translationType);
366  $tpl->setValue('language', Criteria::asValue('=', $lang));
367  $translations = $query->execute(BuildDepth::SINGLE);
368 
369  // delete the found tranlations
370  foreach ($translations as $curTranslation) {
371  $curTranslation->delete();
372  }
373  }
374  }
375 
376  /**
377  * Set a translated value in the given PersistentObject instance.
378  * @param $object The object to set the value on. The object
379  * is supposed to have it's values in the default language.
380  * @param $valueName The name of the value to translate
381  * @param $translation Translation for the value.
382  * @param $useDefaults Boolean whether to use the default language if no
383  * translation is found or not.
384  */
385  private function setTranslatedValue(PersistentObject $object, $valueName, $translation, $useDefaults) {
386  $mapper = $object->getMapper();
387  $isTranslatable = $mapper != null && $mapper->hasAttribute($valueName) ? $mapper->getAttribute($valueName)->hasTag('TRANSLATABLE') : false;
388  if ($isTranslatable) {
389  // empty the value, if the default language values should not be used
390  if (!$useDefaults) {
391  $object->setValue($valueName, null, true);
392  }
393  // translate the value
394  if (!($useDefaults && strlen($translation) == 0)) {
395  $object->setValue($valueName, $translation, true);
396  }
397  }
398  }
399 
400  /**
401  * Save translated values for the given object
402  * @param $object The object to save the translations on
403  * @param $valueName The name of the value to translate
404  * @param $existingTranslation Existing translation instance for the value (might be null).
405  * @param $lang The language of the translations.
406  */
407  private function saveTranslatedValue(PersistentObject $object, $valueName, $existingTranslation, $lang) {
408  $mapper = $object->getMapper();
409  $isTranslatable = $mapper != null && $mapper->hasAttribute($valueName) ? $mapper->getAttribute($valueName)->hasTag('TRANSLATABLE') : false;
410  if ($isTranslatable) {
411  $value = $object->getValue($valueName);
412  $translation = $existingTranslation;
413  $valueIsEmpty = $value === null || $value === '';
414 
415  // if no translation exists and the value is not empty, create a new translation
416  if ($translation == null && !$valueIsEmpty) {
417  $translation = $this->persistenceFacade->create($this->translationType);
418  }
419 
420  // if a translation exists and the value is empty, remove the existing translation
421  if ($translation != null && $valueIsEmpty) {
422  $translation->delete();
423  $translation = null;
424  }
425 
426  if ($translation) {
427  // set all required properties
428  $oid = $object->getOID()->__toString();
429  $translation->setValue('objectid', $oid);
430  $translation->setValue('attribute', $valueName);
431  $translation->setValue('translation', $value);
432  $translation->setValue('language', $lang);
433 
434  // store translation for oid update if necessary (see afterCreate())
435  if (!isset($this->createdTranslations[$oid])) {
436  $this->createdTranslations[$oid] = [];
437  }
438  $this->createdTranslations[$oid][] = $translation;
439  }
440  }
441  }
442 
443  /**
444  * Update oids after create
445  * @param $event
446  */
447  public function afterCreate(PersistenceEvent $event) {
448  if ($event->getAction() == PersistenceAction::CREATE) {
449  $oldOid = $event->getOldOid();
450  $oldOidStr = $oldOid != null ? $oldOid->__toString() : null;
451  if ($oldOidStr != null && isset($this->createdTranslations[$oldOidStr])) {
452  foreach ($this->createdTranslations[$oldOidStr] as $translation) {
453  $translation->setValue('objectid', $event->getObject()->getOID()->__toString());
454  }
455  }
456  }
457  }
458 }
459 ?>
getObject()
Get the object involved.
__construct(PersistenceFacade $persistenceFacade, Configuration $configuration, EventManager $eventManager, $defaultLanguage, $translationType, $languageType)
Configuration.
EventManager is responsible for dispatching events to registered listeners.
deleteTranslation(ObjectId $oid, $lang=null)
NodeValueIterator is used to iterate over all persistent values of a Node (not including relations).
getOID()
Get the object id of the PersistentObject.
__toString()
Get a string representation of the object id.
Definition: ObjectId.php:215
IllegalArgumentException signals an exception in method arguments.
getMapper()
Get the PersistenceMapper of the object.
setValue($name, $value, $forceSet=false, $trackChange=true)
Set the value of an attribute if it exists.
Criteria defines a condition on a PersistentObject's attribute used to select specific instances.
Definition: Criteria.php:21
ObjectId is the unique identifier of an object.
Definition: ObjectId.php:28
BuildDepth values are used to define the depth when loading object trees.
Definition: BuildDepth.php:19
Implementations of Configuration give access to the application configuration.
NodeIterator is used to iterate over a tree/list built of Nodes using a Depth-First-Algorithm.
ConfigurationException signals an exception in the configuration.
afterCreate(PersistenceEvent $event)
Update oids after create.
PersistentEvent signals create/update/delete operations on a persistent entity.
PersistenceFacade defines the interface for PersistenceFacade implementations.
beforeUpdate()
This method is called always before updating the modified object in the store.
saveTranslationImpl(PersistentObject $object, $lang)
Save a translation of a single entity for a specific language.
static getLogger($name)
Get the logger with the given name.
Definition: LogManager.php:37
PersistentObjectProxy is proxy for an PersistentObject instance.
PersistentObject defines the interface of all persistent objects.
LogManager is used to retrieve Logger instances.
Definition: LogManager.php:20
saveTranslation(PersistentObject $object, $lang, $recursive=true)
DefaultLocalization is a Localization implementation that saves translations in the store.
PersistenceAction values are used to define actions on PersistentObject instances.
Localization defines the interface for storing localized entity instances and retrieving them back.
ObjectQuery implements a template based object query.