vendor/pimcore/pimcore/models/DataObject/Localizedfield/Dao.php line 683

Open in your IDE?
  1. <?php
  2. /**
  3.  * Pimcore
  4.  *
  5.  * This source file is available under two different licenses:
  6.  * - GNU General Public License version 3 (GPLv3)
  7.  * - Pimcore Commercial License (PCL)
  8.  * Full copyright and license information is available in
  9.  * LICENSE.md which is distributed with this source code.
  10.  *
  11.  *  @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  12.  *  @license    http://www.pimcore.org/license     GPLv3 and PCL
  13.  */
  14. namespace Pimcore\Model\DataObject\Localizedfield;
  15. use Doctrine\DBAL\Exception\TableNotFoundException;
  16. use Pimcore\Db;
  17. use Pimcore\Db\Helper;
  18. use Pimcore\Logger;
  19. use Pimcore\Model;
  20. use Pimcore\Model\DataObject;
  21. use Pimcore\Model\DataObject\ClassDefinition\Data\CustomResourcePersistingInterface;
  22. use Pimcore\Model\DataObject\ClassDefinition\Data\LazyLoadingSupportInterface;
  23. use Pimcore\Model\DataObject\ClassDefinition\Data\QueryResourcePersistenceAwareInterface;
  24. use Pimcore\Model\DataObject\ClassDefinition\Data\ResourcePersistenceAwareInterface;
  25. use Pimcore\Tool;
  26. /**
  27.  * @internal
  28.  *
  29.  * @property \Pimcore\Model\DataObject\Localizedfield $model
  30.  */
  31. class Dao extends Model\Dao\AbstractDao
  32. {
  33.     use DataObject\ClassDefinition\Helper\Dao;
  34.     use DataObject\Traits\CompositeIndexTrait;
  35.     /**
  36.      * @var array|null
  37.      */
  38.     protected $tableDefinitions null;
  39.     /**
  40.      * @var DataObject\Concrete\Dao\InheritanceHelper
  41.      */
  42.     protected $inheritanceHelper;
  43.     /**
  44.      * @return string
  45.      */
  46.     public function getTableName()
  47.     {
  48.         $context $this->model->getContext();
  49.         if ($context) {
  50.             $containerType $context['containerType'] ?? null;
  51.             if ($containerType === 'fieldcollection') {
  52.                 $containerKey $context['containerKey'];
  53.                 return 'object_collection_'.$containerKey.'_localized_'.$this->model->getClass()->getId();
  54.             } elseif ($containerType === 'objectbrick') {
  55.                 $containerKey $context['containerKey'];
  56.                 return 'object_brick_localized_'.$containerKey.'_'.$this->model->getClass()->getId();
  57.             }
  58.         }
  59.         return 'object_localized_data_'.$this->model->getClass()->getId();
  60.     }
  61.     /**
  62.      * @return string
  63.      */
  64.     public function getQueryTableName()
  65.     {
  66.         $context $this->model->getContext();
  67.         if ($context) {
  68.             $containerType $context['containerType'] ?? null;
  69.             if ($containerType == 'objectbrick') {
  70.                 $containerKey $context['containerKey'];
  71.                 return 'object_brick_localized_query_'.$containerKey.'_'.$this->model->getClass()->getId();
  72.             }
  73.         }
  74.         return 'object_localized_query_'.$this->model->getClass()->getId();
  75.     }
  76.     /**
  77.      * @param array $params
  78.      *
  79.      * @throws \Exception
  80.      */
  81.     public function save($params = [])
  82.     {
  83.         $context $this->model->getContext();
  84.         // if inside a field collection a delete is not necessary as the fieldcollection deletes all entries anyway
  85.         // see Pimcore\Model\DataObject\Fieldcollection\Dao::delete
  86.         $forceUpdate false;
  87.         if ((isset($params['newParent']) && $params['newParent']) || DataObject::isDirtyDetectionDisabled() || $this->model->hasDirtyLanguages(
  88.         ) || $context['containerType'] == 'fieldcollection') {
  89.             $forceUpdate $this->delete(falsetrue);
  90.         }
  91.         $object $this->model->getObject();
  92.         $validLanguages Tool::getValidLanguages();
  93.         if (isset($context['containerType']) && $context['containerType'] === 'fieldcollection') {
  94.             $containerKey $context['containerKey'];
  95.             $container DataObject\Fieldcollection\Definition::getByKey($containerKey);
  96.         } elseif (isset($context['containerType']) && $context['containerType'] === 'objectbrick') {
  97.             $containerKey $context['containerKey'];
  98.             $container DataObject\Objectbrick\Definition::getByKey($containerKey);
  99.         } else {
  100.             $container $this->model->getClass();
  101.         }
  102.         if (!isset($params['owner'])) {
  103.             throw new \Exception('need owner from container implementation');
  104.         }
  105.         $this->model->_setOwner($params['owner']);
  106.         $this->model->_setOwnerFieldname('localizedfields');
  107.         $this->model->_setOwnerLanguage(null);
  108.         /** @var DataObject\ClassDefinition\Data\Localizedfields $localizedfields */
  109.         $localizedfields $container->getFieldDefinition('localizedfields');
  110.         $fieldDefinitions $localizedfields->getFieldDefinitions(
  111.             ['suppressEnrichment' => true]
  112.         );
  113.         /**
  114.          * We temporary enable the runtime cache so we don't have to calculate the tree for each language
  115.          * which is a great performance gain if you have a lot of languages
  116.          */
  117.         DataObject\Concrete\Dao\InheritanceHelper::setUseRuntimeCache(true);
  118.         $ignoreLocalizedQueryFallback \Pimcore\Config::getSystemConfiguration('objects')['ignore_localized_query_fallback'];
  119.         if (!$ignoreLocalizedQueryFallback) {
  120.             foreach ($validLanguages as $validLanguage) {
  121.                 $fallbackLanguages Tool::getFallbackLanguagesFor($validLanguage);
  122.                 foreach ($fallbackLanguages as $fallbackLanguage) {
  123.                     if ($this->model->isLanguageDirty($fallbackLanguage)) {
  124.                         $this->model->markLanguageAsDirty($validLanguage);
  125.                         break;
  126.                     }
  127.                 }
  128.             }
  129.         }
  130.         $flag DataObject\Localizedfield::getGetFallbackValues();
  131.         if (!$ignoreLocalizedQueryFallback) {
  132.             DataObject\Localizedfield::setGetFallbackValues(true);
  133.         }
  134.         foreach ($validLanguages as $language) {
  135.             if (empty($params['newParent'])
  136.                 && !empty($params['isUpdate'])
  137.                 && !$this->model->isLanguageDirty($language)
  138.                 && !$forceUpdate
  139.             ) {
  140.                 continue;
  141.             }
  142.             $inheritedValues DataObject::doGetInheritedValues();
  143.             DataObject::setGetInheritedValues(false);
  144.             $insertData = [
  145.                 'ooo_id' => $this->model->getObject()->getId(),
  146.                 'language' => $language,
  147.             ];
  148.             if ($container instanceof DataObject\Objectbrick\Definition) {
  149.                 $insertData['fieldname'] = $context['fieldname'];
  150.             } elseif ($container instanceof DataObject\Fieldcollection\Definition) {
  151.                 $insertData['fieldname'] = $context['fieldname'];
  152.                 $insertData['index'] = $context['index'];
  153.             }
  154.             foreach ($fieldDefinitions as $fieldName => $fd) {
  155.                 if ($fd instanceof CustomResourcePersistingInterface) {
  156.                     // for fieldtypes which have their own save algorithm eg. relational data types, ...
  157.                     $context $this->model->getContext() ? $this->model->getContext() : [];
  158.                     if (isset($context['containerType']) && ($context['containerType'] === 'fieldcollection' || $context['containerType'] === 'objectbrick')) {
  159.                         $context['subContainerType'] = 'localizedfield';
  160.                     }
  161.                     $isUpdate = isset($params['isUpdate']) && $params['isUpdate'];
  162.                     $childParams $this->getFieldDefinitionParams($fieldName$language, ['isUpdate' => $isUpdate'context' => $context]);
  163.                     if ($fd instanceof DataObject\ClassDefinition\Data\Relations\AbstractRelations) {
  164.                         $saveLocalizedRelations $forceUpdate || ($params['saveRelationalData']['saveLocalizedRelations'] ?? false);
  165.                         if (($saveLocalizedRelations && $container instanceof DataObject\Fieldcollection\Definition)
  166.                             || (((!$container instanceof DataObject\Fieldcollection\Definition || $container instanceof DataObject\Objectbrick\Definition)
  167.                                     && $this->model->isLanguageDirty($language))
  168.                                 || $saveLocalizedRelations)) {
  169.                             if ($saveLocalizedRelations) {
  170.                                 $childParams['forceSave'] = true;
  171.                             }
  172.                             $fd->save($this->model$childParams);
  173.                         }
  174.                     } else {
  175.                         $fd->save($this->model$childParams);
  176.                     }
  177.                 }
  178.                 if ($fd instanceof ResourcePersistenceAwareInterface) {
  179.                     if (is_array($fd->getColumnType())) {
  180.                         $fieldDefinitionParams $this->getFieldDefinitionParams($fieldName$language, ['isUpdate' => ($params['isUpdate'] ?? false)]);
  181.                         $insertDataArray $fd->getDataForResource(
  182.                             $this->model->getLocalizedValue($fieldName$languagetrue),
  183.                             $object,
  184.                             $fieldDefinitionParams
  185.                         );
  186.                         $insertData array_merge($insertData$insertDataArray);
  187.                         $this->model->setLocalizedValue($fieldName$fd->getDataFromResource($insertDataArray$object$fieldDefinitionParams), $languagefalse);
  188.                     } else {
  189.                         $fieldDefinitionParams $this->getFieldDefinitionParams($fieldName$language, ['isUpdate' => ($params['isUpdate'] ?? false)]);
  190.                         $insertData[$fd->getName()] = $fd->getDataForResource(
  191.                             $this->model->getLocalizedValue($fieldName$languagetrue),
  192.                             $object,
  193.                             $fieldDefinitionParams
  194.                         );
  195.                         $this->model->setLocalizedValue($fieldName$fd->getDataFromResource($insertData[$fd->getName()], $object$fieldDefinitionParams), $languagefalse);
  196.                     }
  197.                 }
  198.             }
  199.             $storeTable $this->getTableName();
  200.             $queryTable $this->getQueryTableName().'_'.$language;
  201.             try {
  202.                 if ((isset($params['newParent']) && $params['newParent']) || !isset($params['isUpdate']) || !$params['isUpdate'] || $this->model->isLanguageDirty(
  203.                     $language
  204.                 )) {
  205.                     Helper::insertOrUpdate($this->db$storeTable$insertData);
  206.                 }
  207.             } catch (TableNotFoundException $e) {
  208.                 // if the table doesn't exist -> create it! deferred creation for object bricks ...
  209.                 try {
  210.                     $this->db->rollBack();
  211.                 } catch (\Exception $er) {
  212.                     // PDO adapter throws exceptions if rollback fails
  213.                     Logger::info((string) $er);
  214.                 }
  215.                 $this->createUpdateTable();
  216.                 // throw exception which gets caught in AbstractObject::save() -> retry saving
  217.                 throw new LanguageTableDoesNotExistException('missing table created, start next run ... ;-)');
  218.             }
  219.             if ($container instanceof DataObject\ClassDefinition || $container instanceof DataObject\Objectbrick\Definition) {
  220.                 // query table
  221.                 $data = [];
  222.                 $data['ooo_id'] = $this->model->getObject()->getId();
  223.                 $data['language'] = $language;
  224.                 $this->inheritanceHelper = new DataObject\Concrete\Dao\InheritanceHelper(
  225.                     $object->getClassId(),
  226.                     'ooo_id',
  227.                     $storeTable,
  228.                     $queryTable
  229.                 );
  230.                 $this->inheritanceHelper->resetFieldsToCheck();
  231.                 $sql 'SELECT * FROM '.$queryTable.' WHERE ooo_id = '.$object->getId(
  232.                 )." AND language = '".$language."'";
  233.                 $oldData = [];
  234.                 try {
  235.                     $oldData $this->db->fetchAssociative($sql);
  236.                 } catch (TableNotFoundException $e) {
  237.                     // if the table doesn't exist -> create it!
  238.                     // the following is to ensure consistent data and atomic transactions, while having the flexibility
  239.                     // to add new languages on the fly without saving all classes having localized fields
  240.                     // first we need to roll back all modifications, because otherwise they would be implicitly committed
  241.                     // by the following DDL
  242.                     try {
  243.                         $this->db->rollBack();
  244.                     } catch (\Exception $er) {
  245.                         // PDO adapter throws exceptions if rollback fails
  246.                         Logger::info((string) $er);
  247.                     }
  248.                     // this creates the missing table
  249.                     $this->createUpdateTable();
  250.                     // at this point we throw an exception so that the transaction gets repeated in DataObject::save()
  251.                     throw new LanguageTableDoesNotExistException('missing table created, start next run ... ;-)');
  252.                 }
  253.                 // get fields which shouldn't be updated
  254.                 $untouchable = [];
  255.                 // @TODO: currently we do not support lazyloading in localized fields
  256.                 $inheritanceEnabled $object->getClass()->getAllowInherit();
  257.                 $parentData null;
  258.                 if ($inheritanceEnabled) {
  259.                     // get the next suitable parent for inheritance
  260.                     $parentForInheritance $object->getNextParentForInheritance();
  261.                     if ($parentForInheritance) {
  262.                         // we don't use the getter (built in functionality to get inherited values) because we need to avoid race conditions
  263.                         // we cannot DataObject\AbstractObject::setGetInheritedValues(true); and then $this->model->getLocalizedValue($key, $language)
  264.                         // so we select the data from the parent object using FOR UPDATE, which causes a lock on this row
  265.                         // so the data of the parent cannot be changed while this transaction is on progress
  266.                         $parentData $this->db->fetchAssociative(
  267.                             'SELECT * FROM '.$queryTable.' WHERE ooo_id = ? AND language = ? FOR UPDATE',
  268.                             [$parentForInheritance->getId(), $language]
  269.                         );
  270.                     }
  271.                 }
  272.                 foreach ($fieldDefinitions as $fd) {
  273.                     if ($fd instanceof QueryResourcePersistenceAwareInterface) {
  274.                         $key $fd->getName();
  275.                         // exclude untouchables if value is not an array - this means data has not been loaded
  276.                         if (!in_array($key$untouchable)) {
  277.                             $localizedValue $this->model->getLocalizedValue($key$language$ignoreLocalizedQueryFallback);
  278.                             $insertData $fd->getDataForQueryResource(
  279.                                 $localizedValue,
  280.                                 $object,
  281.                                 $this->getFieldDefinitionParams($key$language)
  282.                             );
  283.                             $isEmpty $fd->isEmpty($localizedValue);
  284.                             if (is_array($insertData)) {
  285.                                 $columnNames array_keys($insertData);
  286.                                 $data array_merge($data$insertData);
  287.                             } else {
  288.                                 $columnNames = [$key];
  289.                                 $data[$key] = $insertData;
  290.                             }
  291.                             // if the current value is empty and we have data from the parent, we just use it
  292.                             if ($isEmpty && $parentData) {
  293.                                 foreach ($columnNames as $columnName) {
  294.                                     if (array_key_exists($columnName$parentData)) {
  295.                                         $data[$columnName] = $parentData[$columnName];
  296.                                         if (is_array($insertData)) {
  297.                                             $insertData[$columnName] = $parentData[$columnName];
  298.                                         } else {
  299.                                             $insertData $parentData[$columnName];
  300.                                         }
  301.                                     }
  302.                                 }
  303.                             }
  304.                             if ($inheritanceEnabled && $fd->getFieldType() != 'calculatedValue') {
  305.                                 //get changed fields for inheritance
  306.                                 if ($fd->isRelationType()) {
  307.                                     if (is_array($insertData)) {
  308.                                         $doInsert false;
  309.                                         foreach ($insertData as $insertDataKey => $insertDataValue) {
  310.                                             $oldDataValue $oldData[$insertDataKey] ?? null;
  311.                                             $parentDataValue $parentData[$insertDataKey] ?? null;
  312.                                             if ($isEmpty && $oldDataValue == $parentDataValue) {
  313.                                                 // do nothing, ... value is still empty and parent data is equal to current data in query table
  314.                                             } elseif ($oldDataValue != $insertDataValue) {
  315.                                                 $doInsert true;
  316.                                                 break;
  317.                                             }
  318.                                         }
  319.                                         if ($doInsert) {
  320.                                             $this->inheritanceHelper->addRelationToCheck(
  321.                                                 $key,
  322.                                                 $fd,
  323.                                                 array_keys($insertData)
  324.                                             );
  325.                                         }
  326.                                     } else {
  327.                                         $oldDataValue $oldData[$key] ?? null;
  328.                                         $parentDataValue $parentData[$key] ?? null;
  329.                                         if ($isEmpty && $oldDataValue == $parentDataValue) {
  330.                                             // do nothing, ... value is still empty and parent data is equal to current data in query table
  331.                                         } elseif ($oldDataValue != $insertData) {
  332.                                             $this->inheritanceHelper->addRelationToCheck($key$fd);
  333.                                         }
  334.                                     }
  335.                                 } else {
  336.                                     if (is_array($insertData)) {
  337.                                         foreach ($insertData as $insertDataKey => $insertDataValue) {
  338.                                             $oldDataValue $oldData[$insertDataKey] ?? null;
  339.                                             $parentDataValue $parentData[$insertDataKey] ?? null;
  340.                                             if ($isEmpty && $oldDataValue == $parentDataValue) {
  341.                                                 // do nothing, ... value is still empty and parent data is equal to current data in query table
  342.                                             } elseif ($oldDataValue != $insertDataValue) {
  343.                                                 $this->inheritanceHelper->addFieldToCheck($insertDataKey$fd);
  344.                                             }
  345.                                         }
  346.                                     } else {
  347.                                         $oldDataValue $oldData[$key] ?? null;
  348.                                         $parentDataValue $parentData[$key] ?? null;
  349.                                         if ($isEmpty && $oldDataValue == $parentDataValue) {
  350.                                             // do nothing, ... value is still empty and parent data is equal to current data in query table
  351.                                         } elseif ($oldDataValue != $insertData) {
  352.                                             // data changed, do check and update
  353.                                             $this->inheritanceHelper->addFieldToCheck($key$fd);
  354.                                         }
  355.                                     }
  356.                                 }
  357.                             }
  358.                         } else {
  359.                             Logger::debug(
  360.                                 'Excluding untouchable query value for object [ '.$this->model->getObjectId() ." ]  key [ $key ] because it has not been loaded"
  361.                             );
  362.                         }
  363.                     }
  364.                 }
  365.                 $queryTable $this->getQueryTableName().'_'.$language;
  366.                 Helper::insertOrUpdate($this->db$queryTable$data);
  367.                 if ($inheritanceEnabled) {
  368.                     $context = isset($params['context']) ? $params['context'] : [];
  369.                     if ($context['containerType'] === 'objectbrick') {
  370.                         $inheritanceRelationContext = [
  371.                             'ownertype' => 'localizedfield',
  372.                             'ownername' => '/objectbrick~' $context['fieldname'] . '/' $context['containerKey'] . '/localizedfield~localizedfield',
  373.                         ];
  374.                     } else {
  375.                         $inheritanceRelationContext = [
  376.                             'ownertype' => 'localizedfield',
  377.                             'ownername' => 'localizedfield',
  378.                         ];
  379.                     }
  380.                     $this->inheritanceHelper->doUpdate($object->getId(), true, [
  381.                         'language' => $language,
  382.                         'inheritanceRelationContext' => $inheritanceRelationContext,
  383.                     ]);
  384.                 }
  385.                 $this->inheritanceHelper->resetFieldsToCheck();
  386.             }
  387.             DataObject::setGetInheritedValues($inheritedValues);
  388.         } // foreach language
  389.         if (!$ignoreLocalizedQueryFallback) {
  390.             DataObject\Localizedfield::setGetFallbackValues($flag);
  391.         }
  392.         DataObject\Concrete\Dao\InheritanceHelper::setUseRuntimeCache(false);
  393.         DataObject\Concrete\Dao\InheritanceHelper::clearRuntimeCache();
  394.     }
  395.     /**
  396.      * @param bool $deleteQuery
  397.      * @param bool $isUpdate
  398.      *
  399.      * @return bool force update
  400.      */
  401.     public function delete($deleteQuery true$isUpdate true)
  402.     {
  403.         if ($isUpdate && !DataObject::isDirtyDetectionDisabled() && !$this->model->hasDirtyFields()) {
  404.             return false;
  405.         }
  406.         $object $this->model->getObject();
  407.         $context $this->model->getContext();
  408.         $container null;
  409.         try {
  410.             if (isset($context['containerType']) && ($context['containerType'] === 'fieldcollection' || $context['containerType'] === 'objectbrick')) {
  411.                 $containerKey $context['containerKey'];
  412.                 if ($context['containerType'] === 'fieldcollection') {
  413.                     $container DataObject\Fieldcollection\Definition::getByKey($containerKey);
  414.                 } else {
  415.                     $container DataObject\Objectbrick\Definition::getByKey($containerKey);
  416.                 }
  417.             } else {
  418.                 $container $object->getClass();
  419.             }
  420.             if ($deleteQuery) {
  421.                 $id $object->getId();
  422.                 $tablename $this->getTableName();
  423.                 if (isset($context['containerType']) && $context['containerType'] === 'objectbrick') {
  424.                     $this->db->delete($tablename, ['ooo_id' => $id'fieldname' => $context['fieldname']]);
  425.                 } else {
  426.                     $this->db->delete($tablename, ['ooo_id' => $id]);
  427.                 }
  428.                 if (!$container instanceof DataObject\Fieldcollection\Definition || $container instanceof DataObject\Objectbrick\Definition) {
  429.                     $validLanguages Tool::getValidLanguages();
  430.                     foreach ($validLanguages as $language) {
  431.                         $queryTable $this->getQueryTableName().'_'.$language;
  432.                         $this->db->delete($queryTable, ['ooo_id' => $id]);
  433.                     }
  434.                 }
  435.             }
  436.             /** @var DataObject\ClassDefinition\Data\Localizedfields $fieldDefinition */
  437.             $fieldDefinition $container->getFieldDefinition('localizedfields', ['suppressEnrichment' => true]);
  438.             $childDefinitions $fieldDefinition->getFieldDefinitions(['suppressEnrichment' => true]);
  439.             if (is_array($childDefinitions)) {
  440.                 foreach ($childDefinitions as $fd) {
  441.                     if ($fd instanceof CustomResourcePersistingInterface) {
  442.                         $params = [
  443.                             'context' => $this->model->getContext() ? $this->model->getContext() : [],
  444.                             'isUpdate' => $isUpdate,
  445.                         ];
  446.                         if (isset($params['context']['containerType']) && ($params['context']['containerType'] === 'fieldcollection' || $params['context']['containerType'] === 'objectbrick')) {
  447.                             $params['context']['subContainerType'] = 'localizedfield';
  448.                         }
  449.                         $fd->delete($object$params);
  450.                     }
  451.                 }
  452.             }
  453.         } catch (\Exception $e) {
  454.             Logger::error((string) $e);
  455.             if ($isUpdate && $e instanceof TableNotFoundException) {
  456.                 try {
  457.                     $this->db->rollBack();
  458.                 } catch (\Exception $er) {
  459.                     // PDO adapter throws exceptions if rollback fails
  460.                     Logger::info((string) $er);
  461.                 }
  462.                 $this->createUpdateTable();
  463.                 // throw exception which gets caught in AbstractObject::save() -> retry saving
  464.                 throw new LanguageTableDoesNotExistException('missing table created, start next run ... ;-)');
  465.             }
  466.         }
  467.         // remove relations
  468.         if (!DataObject::isDirtyDetectionDisabled()) {
  469.             if (!$this->model->hasDirtyFields()) {
  470.                 return false;
  471.             }
  472.         }
  473.         $db Db::get();
  474.         $dirtyLanguageCondition null;
  475.         if ($this->model->allLanguagesAreDirty() ||
  476.             ($container instanceof DataObject\Fieldcollection\Definition)
  477.         ) {
  478.             $dirtyLanguageCondition '';
  479.         } elseif ($this->model->hasDirtyLanguages()) {
  480.             $languageList = [];
  481.             if (is_array($this->model->getDirtyLanguages())) {
  482.                 foreach ($this->model->getDirtyLanguages() as $language => $flag) {
  483.                     if ($flag) {
  484.                         $languageList[] = $db->quote($language);
  485.                     }
  486.                 }
  487.             }
  488.             $dirtyLanguageCondition ' AND position IN('.implode(','$languageList).')';
  489.         }
  490.         if ($container instanceof DataObject\Fieldcollection\Definition) {
  491.             $objectId $object->getId();
  492.             $index $context['index'] ?? $context['containerKey'] ?? null;
  493.             $containerName $context['fieldname'];
  494.             if (!$context['containerType']) {
  495.                 throw new \Exception('no container type set');
  496.             }
  497.             $sql Helper::quoteInto($this->db'src_id = ?'$objectId)." AND ownertype = 'localizedfield' AND "
  498.                 .Helper::quoteInto($this->db,
  499.                     'ownername LIKE ?',
  500.                     '/'.$context['containerType'].'~'.$containerName.'/'.$index.'/%'
  501.                 ).$dirtyLanguageCondition;
  502.             $this->db->executeStatement('DELETE FROM object_relations_'.$object->getClassId() . ' WHERE ' $sql);
  503.             return true;
  504.         }
  505.         $sql 'ownertype = "localizedfield" AND ownername = "localizedfield" and src_id = '.$this->model->getObject()->getId().$dirtyLanguageCondition;
  506.         $this->db->executeStatement('DELETE FROM object_relations_'.$this->model->getObject()->getClassId() . ' WHERE ' $sql);
  507.         return false;
  508.     }
  509.     /**
  510.      * @param DataObject\Concrete|DataObject\Objectbrick\Data\AbstractData|DataObject\Fieldcollection\Data\AbstractData $object
  511.      * @param array $params
  512.      */
  513.     public function load($object$params = [])
  514.     {
  515.         $validLanguages Tool::getValidLanguages();
  516.         foreach ($validLanguages as &$language) {
  517.             $language $this->db->quote($language);
  518.         }
  519.         $context $this->model->getContext();
  520.         if (isset($context['containerType']) && $context['containerType'] === 'fieldcollection') {
  521.             $containerKey $context['containerKey'];
  522.             $index $context['index'];
  523.             $fieldname $context['fieldname'];
  524.             $container DataObject\Fieldcollection\Definition::getByKey($containerKey);
  525.             $data $this->db->fetchAllAssociative(
  526.                 'SELECT * FROM '.$this->getTableName()
  527.                 .' WHERE ooo_id = ? AND language IN ('.implode(
  528.                     ',',
  529.                     $validLanguages
  530.                 ).') AND `fieldname` = ? AND `index` = ?',
  531.                 [
  532.                     $this->model->getObject()->getId(),
  533.                     $fieldname,
  534.                     $index,
  535.                 ]
  536.             );
  537.         } elseif (isset($context['containerType']) && $context['containerType'] === 'objectbrick') {
  538.             $containerKey $context['containerKey'];
  539.             $container DataObject\Objectbrick\Definition::getByKey($containerKey);
  540.             $fieldname $context['fieldname'];
  541.             $data $this->db->fetchAllAssociative(
  542.                 'SELECT * FROM '.$this->getTableName()
  543.                 .' WHERE ooo_id = ? AND language IN ('.implode(','$validLanguages).') AND `fieldname` = ?',
  544.                 [
  545.                     $this->model->getObject()->getId(),
  546.                     $fieldname,
  547.                 ]
  548.             );
  549.         } else {
  550.             $container $this->model->getClass();
  551.             $data $this->db->fetchAllAssociative(
  552.                 'SELECT * FROM '.$this->getTableName().' WHERE ooo_id = ? AND language IN ('.implode(
  553.                     ',',
  554.                     $validLanguages
  555.                 ).')',
  556.                 [$this->model->getObject()->getId()]
  557.             );
  558.         }
  559.         if (!isset($params['owner'])) {
  560.             throw new \Exception('need owner from container implementation');
  561.         }
  562.         $this->model->_setOwner($params['owner']);
  563.         $this->model->_setOwnerFieldname('localizedfields');
  564.         $this->model->_setOwnerLanguage(null);
  565.         foreach ($data as $row) {
  566.             /** @var DataObject\ClassDefinition\Data\Localizedfields $localizedfields */
  567.             $localizedfields $container->getFieldDefinition('localizedfields');
  568.             foreach ($localizedfields->getFieldDefinitions(
  569.                 ['object' => $object'suppressEnrichment' => true]
  570.             ) as $key => $fd) {
  571.                 if ($fd instanceof CustomResourcePersistingInterface) {
  572.                     // datafield has it's own loader
  573.                     $params['language'] = $row['language'];
  574.                     $params['object'] = $object;
  575.                     if (!isset($params['context'])) {
  576.                         $params['context'] = [];
  577.                     }
  578.                     $params['context']['object'] = $object;
  579.                     if ($fd instanceof LazyLoadingSupportInterface && $fd->getLazyLoading()) {
  580.                         $lazyKey $fd->getName() . DataObject\LazyLoadedFieldsInterface::LAZY_KEY_SEPARATOR $row['language'];
  581.                     } else {
  582.                         $value $fd->load($this->model$params);
  583.                         if ($value === || !empty($value)) {
  584.                             $this->model->setLocalizedValue($key$value$row['language'], false);
  585.                         }
  586.                     }
  587.                 }
  588.                 if ($fd instanceof ResourcePersistenceAwareInterface) {
  589.                     if (is_array($fd->getColumnType())) {
  590.                         $multidata = [];
  591.                         foreach ($fd->getColumnType() as $fkey => $fvalue) {
  592.                             $multidata[$key.'__'.$fkey] = $row[$key.'__'.$fkey];
  593.                         }
  594.                         $value $fd->getDataFromResource($multidatanull$this->getFieldDefinitionParams($key$row['language']));
  595.                         $this->model->setLocalizedValue($key$value$row['language'], false);
  596.                     } else {
  597.                         $value $fd->getDataFromResource($row[$key], null$this->getFieldDefinitionParams($key$row['language']));
  598.                         $this->model->setLocalizedValue($key$value$row['language'], false);
  599.                     }
  600.                 }
  601.             }
  602.         }
  603.     }
  604.     public function createLocalizedViews()
  605.     {
  606.         // init
  607.         $languages Tool::getValidLanguages();
  608.         $defaultTable 'object_query_'.$this->model->getClass()->getId();
  609.         $db $this->db;
  610.         /**
  611.          * macro for creating ifnull statement
  612.          *
  613.          * @param string $field
  614.          * @param array $languages
  615.          *
  616.          * @return string
  617.          */
  618.         $getFallbackValue = function ($field, array $languages) use (&$getFallbackValue$db) {
  619.             // init
  620.             $lang array_shift($languages);
  621.             // get fallback for current language
  622.             $fallback count($languages) > 0
  623.                 $getFallbackValue($field$languages)
  624.                 : 'null';
  625.             // create query
  626.             $sql sprintf(
  627.                 'IF(`%s`.`%s` IS NULL OR STRCMP(`%s`.`%s`, "") = 0, %s, `%s`.`%s`)',
  628.                 $lang,
  629.                 $field,
  630.                 $lang,
  631.                 $field,
  632.                 $fallback,
  633.                 $lang,
  634.                 $field
  635.             );
  636.             return $fallback !== 'null'
  637.                 $sql
  638.                 $db->quoteIdentifier($lang).'.'.$db->quoteIdentifier($field);
  639.         };
  640.         foreach ($languages as $language) {
  641.             try {
  642.                 $tablename $this->getQueryTableName().'_'.$language;
  643.                 // get available columns
  644.                 $viewColumns array_merge(
  645.                     $this->db->fetchAllAssociative('SHOW COLUMNS FROM `'.$defaultTable.'`'),
  646.                     $this->db->fetchAllAssociative('SHOW COLUMNS FROM `objects`')
  647.                 );
  648.                 $localizedColumns $this->db->fetchAllAssociative('SHOW COLUMNS FROM `'.$tablename.'`');
  649.                 // get view fields
  650.                 $viewFields = [];
  651.                 foreach ($viewColumns as $row) {
  652.                     $viewFields[] = $this->db->quoteIdentifier($row['Field']);
  653.                 }
  654.                 // create fallback select
  655.                 $localizedFields = [];
  656.                 $fallbackLanguages array_unique(Tool::getFallbackLanguagesFor($language));
  657.                 array_unshift($fallbackLanguages$language);
  658.                 foreach ($localizedColumns as $row) {
  659.                     if ($row['Field'] == 'language' || $row['Field'] == 'ooo_id') {
  660.                         $localizedFields[] = $db->quoteIdentifier($language).'.'.$db->quoteIdentifier($row['Field']);
  661.                     } else {
  662.                         $localizedFields[] = $getFallbackValue($row['Field'], $fallbackLanguages).sprintf(
  663.                             ' as "%s"',
  664.                             $row['Field']
  665.                         );
  666.                     }
  667.                 }
  668.                 // create view select fields
  669.                 $selectViewFields implode(','array_merge($viewFields$localizedFields));
  670.                 // create view
  671.                 $viewQuery = <<<QUERY
  672. CREATE OR REPLACE VIEW `object_localized_{$this->model->getClass()->getId()}_{$language}` AS
  673. SELECT {$selectViewFields}
  674. FROM `{$defaultTable}`
  675.     JOIN `objects`
  676.         ON (`objects`.`o_id` = `{$defaultTable}`.`oo_id`)
  677. QUERY;
  678.                 // join fallback languages
  679.                 foreach ($fallbackLanguages as $lang) {
  680.                     $viewQuery .= <<<QUERY
  681. LEFT JOIN {$this->getQueryTableName()}_{$lang} as `{$lang}`
  682.     ON( 1
  683.         AND {$defaultTable}.oo_id = {$lang}.ooo_id
  684.     )
  685. QUERY;
  686.                 }
  687.                 // execute
  688.                 $this->db->executeQuery($viewQuery);
  689.             } catch (\Exception $e) {
  690.                 Logger::error((string) $e);
  691.             }
  692.         }
  693.     }
  694.     /**
  695.      * @param array $params
  696.      *
  697.      * @throws \Exception
  698.      */
  699.     public function createUpdateTable($params = [])
  700.     {
  701.         $table $this->getTableName();
  702.         $context $this->model->getContext();
  703.         if (isset($context['containerType']) && ($context['containerType'] === 'fieldcollection' || $context['containerType'] === 'objectbrick')) {
  704.             $this->db->executeQuery(
  705.                 'CREATE TABLE IF NOT EXISTS `'.$table."` (
  706.               `ooo_id` int(11) UNSIGNED NOT NULL default '0',
  707.               `index` INT(11) NOT NULL DEFAULT '0',
  708.               `fieldname` VARCHAR(190) NOT NULL DEFAULT '',
  709.               `language` varchar(10) NOT NULL DEFAULT '',
  710.               PRIMARY KEY (`ooo_id`, `language`, `index`, `fieldname`),
  711.               INDEX `index` (`index`),
  712.               INDEX `fieldname` (`fieldname`),
  713.               INDEX `language` (`language`),
  714.               CONSTRAINT `".self::getForeignKeyName($table'ooo_id').'` FOREIGN KEY (`ooo_id`) REFERENCES objects (`o_id`) ON DELETE CASCADE
  715.             ) DEFAULT CHARSET=utf8mb4;'
  716.             );
  717.         } else {
  718.             $this->db->executeQuery(
  719.                 'CREATE TABLE IF NOT EXISTS `'.$table."` (
  720.               `ooo_id` int(11) UNSIGNED NOT NULL default '0',
  721.               `language` varchar(10) NOT NULL DEFAULT '',
  722.               PRIMARY KEY (`ooo_id`,`language`),
  723.               INDEX `language` (`language`),
  724.               CONSTRAINT `".self::getForeignKeyName($table'ooo_id').'` FOREIGN KEY (`ooo_id`) REFERENCES objects (`o_id`) ON DELETE CASCADE
  725.             ) DEFAULT CHARSET=utf8mb4;'
  726.             );
  727.         }
  728.         $this->handleEncryption($this->model->getClass(), [$table]);
  729.         $existingColumns $this->getValidTableColumns($tablefalse); // no caching of table definition
  730.         $columnsToRemove $existingColumns;
  731.         DataObject\ClassDefinition\Service::updateTableDefinitions($this->tableDefinitions, ([$table]));
  732.         if (isset($context['containerType']) && ($context['containerType'] === 'fieldcollection' || $context['containerType'] === 'objectbrick')) {
  733.             $protectedColumns = ['ooo_id''language''index''fieldname'];
  734.             $containerKey $context['containerKey'];
  735.             if ($context['containerType'] === 'fieldcollection') {
  736.                 $container DataObject\Fieldcollection\Definition::getByKey($containerKey);
  737.             } else {
  738.                 $container DataObject\Objectbrick\Definition::getByKey($containerKey);
  739.             }
  740.         } else {
  741.             $protectedColumns = ['ooo_id''language'];
  742.             $container $this->model->getClass();
  743.         }
  744.         /** @var DataObject\ClassDefinition\Data\Localizedfields $localizedFieldDefinition */
  745.         $localizedFieldDefinition $container->getFieldDefinition('localizedfields', ['suppressEnrichment' => true]);
  746.         if ($localizedFieldDefinition instanceof DataObject\ClassDefinition\Data\Localizedfields) {
  747.             foreach ($localizedFieldDefinition->getFieldDefinitions(['suppressEnrichment' => true]) as $value) {
  748.                 if ($value instanceof ResourcePersistenceAwareInterface) {
  749.                     if ($value->getColumnType()) {
  750.                         $key $value->getName();
  751.                         if (is_array($value->getColumnType())) {
  752.                             // if a datafield requires more than one column
  753.                             foreach ($value->getColumnType() as $fkey => $fvalue) {
  754.                                 $this->addModifyColumn($table$key '__' $fkey$fvalue'''NULL');
  755.                                 $protectedColumns[] = $key '__' $fkey;
  756.                             }
  757.                         } else {
  758.                             $this->addModifyColumn($table$key$value->getColumnType(), '''NULL');
  759.                             $protectedColumns[] = $key;
  760.                         }
  761.                         $this->addIndexToField($value$table'getColumnType'truetrue);
  762.                     }
  763.                 }
  764.             }
  765.         }
  766.         $this->removeIndices($table$columnsToRemove$protectedColumns);
  767.         $this->removeUnusedColumns($table$columnsToRemove$protectedColumns);
  768.         $validLanguages Tool::getValidLanguages();
  769.         if ($container instanceof DataObject\ClassDefinition || $container instanceof DataObject\Objectbrick\Definition) {
  770.             foreach ($validLanguages as &$language) {
  771.                 $queryTable $this->getQueryTableName();
  772.                 $queryTable .= '_'.$language;
  773.                 $this->db->executeQuery(
  774.                     'CREATE TABLE IF NOT EXISTS `'.$queryTable."` (
  775.                       `ooo_id` int(11) UNSIGNED NOT NULL default '0',
  776.                       `language` varchar(10) NOT NULL DEFAULT '',
  777.                       PRIMARY KEY (`ooo_id`,`language`),
  778.                       INDEX `language` (`language`),
  779.                       CONSTRAINT `".self::getForeignKeyName($queryTable'ooo_id').'` FOREIGN KEY (`ooo_id`) REFERENCES objects (`o_id`) ON DELETE CASCADE
  780.                     ) DEFAULT CHARSET=utf8mb4;'
  781.                 );
  782.                 $this->handleEncryption($this->model->getClass(), [$queryTable]);
  783.                 // create object table if not exists
  784.                 $protectedColumns = ['ooo_id''language'];
  785.                 $existingColumns $this->getValidTableColumns($queryTablefalse); // no caching of table definition
  786.                 $columnsToRemove $existingColumns;
  787.                 DataObject\ClassDefinition\Service::updateTableDefinitions($this->tableDefinitions, [$queryTable]);
  788.                 $fieldDefinitions = [];
  789.                 if ($container instanceof DataObject\Objectbrick\Definition) {
  790.                     $containerKey $context['containerKey'];
  791.                     $container DataObject\Objectbrick\Definition::getByKey($containerKey);
  792.                     /** @var DataObject\ClassDefinition\Data\Localizedfields $localizedfields */
  793.                     $localizedfields $container->getFieldDefinition('localizedfields', ['suppressEnrichment' => true]);
  794.                     $fieldDefinitions $localizedfields->getFieldDefinitions(['suppressEnrichment' => true]);
  795.                 } else {
  796.                     /** @var DataObject\ClassDefinition\Data\Localizedfields $localizedfields */
  797.                     $localizedfields $this->model->getClass()->getFieldDefinition('localizedfields', ['suppressEnrichment' => true]);
  798.                     if ($localizedfields instanceof DataObject\ClassDefinition\Data\Localizedfields) {
  799.                         $fieldDefinitions $localizedfields->getFieldDefinitions(['suppressEnrichment' => true]);
  800.                     }
  801.                 }
  802.                 // add non existing columns in the table
  803.                 if (is_array($fieldDefinitions) && count($fieldDefinitions)) {
  804.                     foreach ($fieldDefinitions as $value) {
  805.                         if ($value instanceof DataObject\ClassDefinition\Data\QueryResourcePersistenceAwareInterface) {
  806.                             $key $value->getName();
  807.                             // if a datafield requires more than one column in the query table
  808.                             if (is_array($value->getQueryColumnType())) {
  809.                                 foreach ($value->getQueryColumnType() as $fkey => $fvalue) {
  810.                                     $this->addModifyColumn($queryTable$key.'__'.$fkey$fvalue'''NULL');
  811.                                     $protectedColumns[] = $key.'__'.$fkey;
  812.                                 }
  813.                             } elseif ($value->getQueryColumnType()) {
  814.                                 $this->addModifyColumn($queryTable$key$value->getQueryColumnType(), '''NULL');
  815.                                 $protectedColumns[] = $key;
  816.                             }
  817.                             // add indices
  818.                             $this->addIndexToField($value$queryTable'getQueryColumnType');
  819.                         }
  820.                     }
  821.                 }
  822.                 // remove unused columns in the table
  823.                 $this->removeUnusedColumns($queryTable$columnsToRemove$protectedColumns);
  824.                 if ($container instanceof DataObject\ClassDefinition) {
  825.                     $this->updateCompositeIndices($queryTable'localized_query'$this->model->getClass()->getCompositeIndices());
  826.                 }
  827.             }
  828.         }
  829.         if ($container instanceof DataObject\ClassDefinition) {
  830.             $this->updateCompositeIndices($table'localized_store'$this->model->getClass()->getCompositeIndices());
  831.             $this->createLocalizedViews();
  832.         }
  833.         $this->tableDefinitions null;
  834.     }
  835.     /**
  836.      * @param string $fieldname
  837.      * @param string $language
  838.      * @param array $extraParams
  839.      *
  840.      * @return array
  841.      */
  842.     public function getFieldDefinitionParams(string $fieldnamestring $language$extraParams = [])
  843.     {
  844.         return array_merge(
  845.             [
  846.                 'owner' => $this->model,
  847.                 'fieldname' => $fieldname,
  848.                 'language' => $language,
  849.             ],
  850.             $extraParams
  851.         );
  852.     }
  853. }