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

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