vendor/pimcore/pimcore/models/DataObject/ClassDefinition/Data/Relations/AbstractRelations.php line 139

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\ClassDefinition\Data\Relations;
  15. use Pimcore\Db;
  16. use Pimcore\Logger;
  17. use Pimcore\Model\DataObject;
  18. use Pimcore\Model\DataObject\ClassDefinition\Data;
  19. use Pimcore\Model\DataObject\ClassDefinition\Data\CustomResourcePersistingInterface;
  20. use Pimcore\Model\Element;
  21. abstract class AbstractRelations extends Data implements
  22. CustomResourcePersistingInterface,
  23. DataObject\ClassDefinition\PathFormatterAwareInterface,
  24. Data\LazyLoadingSupportInterface,
  25. Data\EqualComparisonInterface,
  26. Data\IdRewriterInterface
  27. {
  28. use DataObject\Traits\ContextPersistenceTrait;
  29. const RELATION_ID_SEPARATOR = '$$';
  30. /**
  31. * Set of allowed classes
  32. *
  33. * @internal
  34. *
  35. * @var array
  36. */
  37. public $classes = [];
  38. /**
  39. * Optional path formatter class
  40. *
  41. * @internal
  42. *
  43. * @var null|string
  44. */
  45. public $pathFormatterClass;
  46. /**
  47. * @return array[
  48. * 'classes' => string,
  49. * ]
  50. */
  51. public function getClasses()
  52. {
  53. return $this->classes ?: [];
  54. }
  55. /**
  56. * @param array $classes
  57. *
  58. * @return $this
  59. */
  60. public function setClasses($classes)
  61. {
  62. $this->classes = Element\Service::fixAllowedTypes($classes, 'classes');
  63. return $this;
  64. }
  65. /**
  66. * {@inheritdoc}
  67. */
  68. public function getLazyLoading()
  69. {
  70. return true;
  71. }
  72. /**
  73. * {@inheritdoc}
  74. */
  75. public function save($object, $params = [])
  76. {
  77. if (isset($params['isUntouchable']) && $params['isUntouchable']) {
  78. return;
  79. }
  80. if (!isset($params['context'])) {
  81. $params['context'] = null;
  82. }
  83. $context = $params['context'];
  84. if (!DataObject::isDirtyDetectionDisabled() && $object instanceof Element\DirtyIndicatorInterface) {
  85. if (!isset($context['containerType']) || $context['containerType'] !== 'fieldcollection') {
  86. if ($object instanceof DataObject\Localizedfield) {
  87. if ($object->getObject() instanceof Element\DirtyIndicatorInterface && !$object->hasDirtyFields()) {
  88. return;
  89. }
  90. } elseif ($this->supportsDirtyDetection() && !$object->isFieldDirty($this->getName())) {
  91. return;
  92. }
  93. }
  94. }
  95. $data = $this->getDataFromObjectParam($object, $params);
  96. $relations = $this->prepareDataForPersistence($data, $object, $params);
  97. if (is_array($relations) && !empty($relations)) {
  98. $db = Db::get();
  99. foreach ($relations as $relation) {
  100. $this->enrichDataRow($object, $params, $classId, $relation);
  101. // relation needs to be an array with src_id, dest_id, type, fieldname
  102. try {
  103. $db->insert('object_relations_' . $classId, $relation);
  104. } catch (\Exception $e) {
  105. Logger::error('It seems that the relation ' . $relation['src_id'] . ' => ' . $relation['dest_id']
  106. . ' (fieldname: ' . $this->getName() . ') already exist -> please check immediately!');
  107. Logger::error($e);
  108. // try it again with an update if the insert fails, shouldn't be the case, but it seems that
  109. // sometimes the insert throws an exception
  110. throw $e;
  111. }
  112. }
  113. }
  114. }
  115. /**
  116. * {@inheritdoc}
  117. */
  118. public function load($object, $params = [])
  119. {
  120. $data = null;
  121. $relations = [];
  122. if ($object instanceof DataObject\Concrete) {
  123. $relations = $object->retrieveRelationData(['fieldname' => $this->getName(), 'ownertype' => 'object']);
  124. } elseif ($object instanceof DataObject\Fieldcollection\Data\AbstractData) {
  125. $relations = $object->getObject()->retrieveRelationData(['fieldname' => $this->getName(), 'ownertype' => 'fieldcollection', 'ownername' => $object->getFieldname(), 'position' => $object->getIndex()]);
  126. } elseif ($object instanceof DataObject\Localizedfield) {
  127. $context = $params['context'] ?? null;
  128. if (isset($context['containerType']) && (($context['containerType'] === 'fieldcollection' || $context['containerType'] === 'objectbrick'))) {
  129. $fieldname = $context['fieldname'] ?? null;
  130. if ($context['containerType'] === 'fieldcollection') {
  131. $index = $context['index'] ?? null;
  132. $filter = '/' . $context['containerType'] . '~' . $fieldname . '/' . $index . '/%';
  133. } else {
  134. $filter = '/' . $context['containerType'] . '~' . $fieldname . '/%';
  135. }
  136. $relations = $object->getObject()->retrieveRelationData(['fieldname' => $this->getName(), 'ownertype' => 'localizedfield', 'ownername' => $filter, 'position' => $params['language']]);
  137. } else {
  138. $relations = $object->getObject()->retrieveRelationData(['fieldname' => $this->getName(), 'ownertype' => 'localizedfield', 'position' => $params['language']]);
  139. }
  140. } elseif ($object instanceof DataObject\Objectbrick\Data\AbstractData) {
  141. $relations = $object->getObject()->retrieveRelationData(['fieldname' => $this->getName(), 'ownertype' => 'objectbrick', 'ownername' => $object->getFieldname(), 'position' => $object->getType()]);
  142. }
  143. // using PHP sorting to order the relations, because "ORDER BY index ASC" in the queries above will cause a
  144. // filesort in MySQL which is extremely slow especially when there are millions of relations in the database
  145. usort($relations, function ($a, $b) {
  146. if ($a['index'] == $b['index']) {
  147. return 0;
  148. }
  149. return ($a['index'] < $b['index']) ? -1 : 1;
  150. });
  151. $data = $this->loadData($relations, $object, $params);
  152. if ($object instanceof Element\DirtyIndicatorInterface && $data['dirty']) {
  153. $object->markFieldDirty($this->getName(), true);
  154. }
  155. return $data['data'];
  156. }
  157. /**
  158. * @internal
  159. *
  160. * @param array $data
  161. * @param DataObject\Concrete $object
  162. * @param array $params
  163. *
  164. * @return mixed
  165. */
  166. abstract protected function loadData(array $data, $object = null, $params = []);
  167. /**
  168. * @internal
  169. *
  170. * @param array|Element\ElementInterface $data
  171. * @param DataObject\Concrete $object
  172. * @param array $params
  173. *
  174. * @return mixed
  175. */
  176. abstract protected function prepareDataForPersistence($data, $object = null, $params = []);
  177. /**
  178. * {@inheritdoc}
  179. */
  180. public function delete($object, $params = [])
  181. {
  182. }
  183. /**
  184. * Rewrites id from source to target, $idMapping contains
  185. * array(
  186. * "document" => array(
  187. * SOURCE_ID => TARGET_ID,
  188. * SOURCE_ID => TARGET_ID
  189. * ),
  190. * "object" => array(...),
  191. * "asset" => array(...)
  192. * )
  193. *
  194. * @internal
  195. *
  196. * @param mixed $data
  197. * @param array $idMapping
  198. *
  199. * @return array
  200. */
  201. protected function rewriteIdsService($data, $idMapping)
  202. {
  203. if (is_array($data)) {
  204. foreach ($data as &$element) {
  205. $id = $element->getId();
  206. $type = Element\Service::getElementType($element);
  207. if (array_key_exists($type, $idMapping) && array_key_exists($id, $idMapping[$type])) {
  208. $element = Element\Service::getElementById($type, $idMapping[$type][$id]);
  209. }
  210. }
  211. }
  212. return $data;
  213. }
  214. /**
  215. * {@inheritdoc}
  216. */
  217. public function getPathFormatterClass(): ?string
  218. {
  219. return $this->pathFormatterClass;
  220. }
  221. /**
  222. * @param null|string $pathFormatterClass
  223. */
  224. public function setPathFormatterClass($pathFormatterClass)
  225. {
  226. $this->pathFormatterClass = $pathFormatterClass;
  227. }
  228. /**
  229. * {@inheritdoc}
  230. */
  231. public function getDataForSearchIndex($object, $params = [])
  232. {
  233. return '';
  234. }
  235. /**
  236. * {@inheritdoc}
  237. */
  238. public function appendData($existingData, $additionalData)
  239. {
  240. $newData = [];
  241. if (!is_array($existingData)) {
  242. $existingData = [];
  243. }
  244. $map = [];
  245. /** @var Element\ElementInterface $item */
  246. foreach ($existingData as $item) {
  247. $key = $this->buildUniqueKeyForAppending($item);
  248. $map[$key] = 1;
  249. $newData[] = $item;
  250. }
  251. if (is_array($additionalData)) {
  252. foreach ($additionalData as $item) {
  253. $key = $this->buildUniqueKeyForAppending($item);
  254. if (!isset($map[$key])) {
  255. $newData[] = $item;
  256. }
  257. }
  258. }
  259. return $newData;
  260. }
  261. /**
  262. * {@inheritdoc}
  263. */
  264. public function removeData($existingData, $removeData)
  265. {
  266. $newData = [];
  267. if (!is_array($existingData)) {
  268. $existingData = [];
  269. }
  270. $removeMap = [];
  271. /** @var Element\ElementInterface $item */
  272. foreach ($removeData as $item) {
  273. $key = $this->buildUniqueKeyForAppending($item);
  274. $removeMap[$key] = 1;
  275. }
  276. $newData = [];
  277. /** @var Element\ElementInterface $item */
  278. foreach ($existingData as $item) {
  279. $key = $this->buildUniqueKeyForAppending($item);
  280. if (!isset($removeMap[$key])) {
  281. $newData[] = $item;
  282. }
  283. }
  284. return $newData;
  285. }
  286. /**
  287. * @internal
  288. *
  289. * @param Element\ElementInterface $item
  290. *
  291. * @return string
  292. */
  293. protected function buildUniqueKeyForAppending($item)
  294. {
  295. $elementType = Element\Service::getElementType($item);
  296. $id = $item->getId();
  297. return $elementType . $id;
  298. }
  299. /**
  300. * {@inheritdoc}
  301. */
  302. public function isEqual($array1, $array2): bool
  303. {
  304. $array1 = array_filter(is_array($array1) ? $array1 : []);
  305. $array2 = array_filter(is_array($array2) ? $array2 : []);
  306. $count1 = count($array1);
  307. $count2 = count($array2);
  308. if ($count1 != $count2) {
  309. return false;
  310. }
  311. $values1 = array_values($array1);
  312. $values2 = array_values($array2);
  313. for ($i = 0; $i < $count1; $i++) {
  314. /** @var Element\ElementInterface $el1 */
  315. $el1 = $values1[$i];
  316. /** @var Element\ElementInterface $el2 */
  317. $el2 = $values2[$i];
  318. if (! ($el1->getType() == $el2->getType() && ($el1->getId() == $el2->getId()))) {
  319. return false;
  320. }
  321. }
  322. return true;
  323. }
  324. /**
  325. * {@inheritdoc}
  326. */
  327. public function supportsDirtyDetection()
  328. {
  329. return true;
  330. }
  331. /**
  332. * @internal
  333. *
  334. * @param DataObject\Fieldcollection\Data\AbstractData $item
  335. *
  336. * @throws \Exception
  337. */
  338. protected function loadLazyFieldcollectionField(DataObject\Fieldcollection\Data\AbstractData $item)
  339. {
  340. if ($item->getObject()) {
  341. /** @var DataObject\Fieldcollection|null $container */
  342. $container = $item->getObject()->getObjectVar($item->getFieldname());
  343. if ($container) {
  344. $container->loadLazyField($item->getObject(), $item->getType(), $item->getFieldname(), $item->getIndex(), $this->getName());
  345. } else {
  346. // if container is not available we assume that it is a newly set item
  347. $item->markLazyKeyAsLoaded($this->getName());
  348. }
  349. }
  350. }
  351. /**
  352. * @internal
  353. *
  354. * @param DataObject\Objectbrick\Data\AbstractData $item
  355. *
  356. * @throws \Exception
  357. */
  358. protected function loadLazyBrickField(DataObject\Objectbrick\Data\AbstractData $item)
  359. {
  360. if ($item->getObject()) {
  361. /** @var DataObject\Objectbrick|null $container */
  362. $container = $item->getObject()->getObjectVar($item->getFieldname());
  363. if ($container) {
  364. $container->loadLazyField($item->getType(), $item->getFieldname(), $this->getName());
  365. } else {
  366. $item->markLazyKeyAsLoaded($this->getName());
  367. }
  368. }
  369. }
  370. /**
  371. * checks for multiple assignments and throws an exception in case the rules are violated.
  372. *
  373. * @internal
  374. *
  375. * @param array|null $data
  376. *
  377. * @throws Element\ValidationException
  378. */
  379. public function performMultipleAssignmentCheck($data)
  380. {
  381. if (is_array($data)) {
  382. if (!method_exists($this, 'getAllowMultipleAssignments') || !$this->getAllowMultipleAssignments()) {
  383. $relationItems = [];
  384. $fieldName = $this->getName();
  385. foreach ($data as $item) {
  386. $elementHash = null;
  387. if ($item instanceof DataObject\Data\ObjectMetadata || $item instanceof DataObject\Data\ElementMetadata) {
  388. if ($item->getElement() instanceof Element\ElementInterface) {
  389. $elementHash = Element\Service::getElementHash($item->getElement());
  390. }
  391. } elseif ($item instanceof Element\ElementInterface) {
  392. $elementHash = Element\Service::getElementHash($item);
  393. }
  394. if ($elementHash === null) {
  395. throw new Element\ValidationException('Passing relations without ID or type not allowed anymore!');
  396. } elseif (!isset($relationItems[$elementHash])) {
  397. $relationItems[$elementHash] = $item;
  398. } else {
  399. $message = 'Passing relations multiple times not allowed anymore: ' . $elementHash
  400. . ' multiple times in field ' . $fieldName;
  401. if (method_exists($this, 'getAllowMultipleAssignments')) {
  402. $message .= ", Reason: 'Allow Multiple Assignments' setting is disabled in class definition. ";
  403. }
  404. throw new Element\ValidationException($message);
  405. }
  406. }
  407. }
  408. }
  409. }
  410. /**
  411. * {@inheritdoc}
  412. */
  413. public function getParameterTypeDeclaration(): ?string
  414. {
  415. return '?array';
  416. }
  417. /**
  418. * {@inheritdoc}
  419. */
  420. public function getReturnTypeDeclaration(): ?string
  421. {
  422. return 'array';
  423. }
  424. /**
  425. * @return string|null
  426. */
  427. public function getPhpdocInputType(): ?string
  428. {
  429. if ($this->getPhpdocType()) {
  430. return $this->getPhpdocType();
  431. }
  432. return null;
  433. }
  434. /**
  435. * {@inheritdoc}
  436. */
  437. public function getPhpdocReturnType(): ?string
  438. {
  439. if ($phpdocType = $this->getPhpdocType()) {
  440. return $phpdocType;
  441. }
  442. return null;
  443. }
  444. /**
  445. * @internal
  446. *
  447. * @return string
  448. */
  449. abstract protected function getPhpdocType();
  450. }