vendor/pimcore/pimcore/models/DataObject/ClassDefinition/Data/ManyToManyObjectRelation.php line 503

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;
  15. use Pimcore\Model;
  16. use Pimcore\Model\DataObject;
  17. use Pimcore\Model\DataObject\ClassDefinition\Data\Relations\AbstractRelations;
  18. use Pimcore\Model\DataObject\Concrete;
  19. use Pimcore\Model\Element;
  20. use Pimcore\Normalizer\NormalizerInterface;
  21. class ManyToManyObjectRelation extends AbstractRelations implements QueryResourcePersistenceAwareInterface, OptimizedAdminLoadingInterface, TypeDeclarationSupportInterface, VarExporterInterface, NormalizerInterface, IdRewriterInterface, PreGetDataInterface, PreSetDataInterface, LayoutDefinitionEnrichmentInterface
  22. {
  23. use Model\DataObject\ClassDefinition\Data\Extension\Relation;
  24. use Extension\QueryColumnType;
  25. use DataObject\ClassDefinition\Data\Relations\AllowObjectRelationTrait;
  26. use DataObject\ClassDefinition\Data\Relations\ManyToManyRelationTrait;
  27. /**
  28. * Static type of this element
  29. *
  30. * @internal
  31. *
  32. * @var string
  33. */
  34. public $fieldtype = 'manyToManyObjectRelation';
  35. /**
  36. * @internal
  37. *
  38. * @var string|int
  39. */
  40. public $width = 0;
  41. /**
  42. * Type for the column to query
  43. *
  44. * @internal
  45. *
  46. * @var string|int
  47. */
  48. public $height = 0;
  49. /**
  50. * @internal
  51. *
  52. * @var int
  53. */
  54. public $maxItems;
  55. /**
  56. * Type for the column to query
  57. *
  58. * @internal
  59. *
  60. * @var string
  61. */
  62. public $queryColumnType = 'text';
  63. /**
  64. * @internal
  65. *
  66. * @var bool
  67. */
  68. public $relationType = true;
  69. /**
  70. * @internal
  71. *
  72. * @var string|null
  73. */
  74. public $visibleFields;
  75. /**
  76. * @internal
  77. *
  78. * @var bool
  79. */
  80. public $allowToCreateNewObject = true;
  81. /**
  82. * @internal
  83. *
  84. * @var bool
  85. */
  86. public $optimizedAdminLoading = false;
  87. /**
  88. * @internal
  89. *
  90. * @var bool
  91. */
  92. public $enableTextSelection = false;
  93. /**
  94. * @internal
  95. *
  96. * @var array
  97. */
  98. public $visibleFieldDefinitions = [];
  99. /**
  100. * @return bool
  101. */
  102. public function getObjectsAllowed()
  103. {
  104. return true;
  105. }
  106. /**
  107. * {@inheritdoc}
  108. */
  109. protected function prepareDataForPersistence($data, $object = null, $params = [])
  110. {
  111. $return = [];
  112. if (is_array($data) && count($data) > 0) {
  113. $counter = 1;
  114. foreach ($data as $object) {
  115. if ($object instanceof DataObject\Concrete) {
  116. $return[] = [
  117. 'dest_id' => $object->getId(),
  118. 'type' => 'object',
  119. 'fieldname' => $this->getName(),
  120. 'index' => $counter,
  121. ];
  122. }
  123. $counter++;
  124. }
  125. return $return;
  126. } elseif (is_array($data) && count($data) === 0) {
  127. //give empty array if data was not null
  128. return [];
  129. } else {
  130. //return null if data was null - this indicates data was not loaded
  131. return null;
  132. }
  133. }
  134. /**
  135. * {@inheritdoc}
  136. */
  137. protected function loadData(array $data, $object = null, $params = [])
  138. {
  139. $objects = [
  140. 'dirty' => false,
  141. 'data' => [],
  142. ];
  143. foreach ($data as $relation) {
  144. $o = DataObject::getById($relation['dest_id']);
  145. if ($o instanceof DataObject\Concrete) {
  146. $objects['data'][] = $o;
  147. } else {
  148. $objects['dirty'] = true;
  149. }
  150. }
  151. //must return array - otherwise this means data is not loaded
  152. return $objects;
  153. }
  154. /**
  155. * @see QueryResourcePersistenceAwareInterface::getDataForQueryResource
  156. *
  157. * @param mixed $data
  158. * @param null|DataObject\Concrete $object
  159. * @param mixed $params
  160. *
  161. * @throws \Exception
  162. *
  163. * @return string|null
  164. */
  165. public function getDataForQueryResource($data, $object = null, $params = [])
  166. {
  167. //return null when data is not set
  168. if (!$data) {
  169. return null;
  170. }
  171. $ids = [];
  172. if (is_array($data)) {
  173. foreach ($data as $relation) {
  174. if ($relation instanceof DataObject\Concrete) {
  175. $ids[] = $relation->getId();
  176. }
  177. }
  178. return ',' . implode(',', $ids) . ',';
  179. }
  180. throw new \Exception('invalid data passed to getDataForQueryResource - must be array and it is: ' . print_r($data, true));
  181. }
  182. /**
  183. * @see Data::getDataForEditmode
  184. *
  185. * @param array $data
  186. * @param null|DataObject\Concrete $object
  187. * @param mixed $params
  188. *
  189. * @return array
  190. */
  191. public function getDataForEditmode($data, $object = null, $params = [])
  192. {
  193. $return = [];
  194. $visibleFieldsArray = $this->getVisibleFields() ? explode(',', $this->getVisibleFields()) : [];
  195. $gridFields = (array)$visibleFieldsArray;
  196. // add data
  197. if (is_array($data) && count($data) > 0) {
  198. foreach ($data as $referencedObject) {
  199. if ($referencedObject instanceof DataObject\Concrete) {
  200. $return[] = DataObject\Service::gridObjectData($referencedObject, $gridFields, null, ['purpose' => 'editmode']);
  201. }
  202. }
  203. }
  204. return $return;
  205. }
  206. /**
  207. * @see Data::getDataFromEditmode
  208. *
  209. * @param array|null|false $data
  210. * @param null|DataObject\Concrete $object
  211. * @param mixed $params
  212. *
  213. * @return array|null
  214. */
  215. public function getDataFromEditmode($data, $object = null, $params = [])
  216. {
  217. //if not set, return null
  218. if ($data === null || $data === false) {
  219. return null;
  220. }
  221. $objects = [];
  222. if (is_array($data) && count($data) > 0) {
  223. foreach ($data as $object) {
  224. $o = DataObject::getById($object['id']);
  225. if ($o) {
  226. $objects[] = $o;
  227. }
  228. }
  229. }
  230. //must return array if data shall be set
  231. return $objects;
  232. }
  233. /**
  234. * @see Data::getDataFromEditmode
  235. *
  236. * @param array $data
  237. * @param null|DataObject\Concrete $object
  238. * @param mixed $params
  239. *
  240. * @return array
  241. */
  242. public function getDataFromGridEditor($data, $object = null, $params = [])
  243. {
  244. return $this->getDataFromEditmode($data, $object, $params);
  245. }
  246. /**
  247. * @param array|null $data
  248. * @param DataObject\Concrete|null $object
  249. * @param mixed $params
  250. *
  251. * @return array|null
  252. */
  253. public function getDataForGrid($data, $object = null, $params = [])
  254. {
  255. return $this->getDataForEditmode($data, $object, $params);
  256. }
  257. /**
  258. * @see Data::getVersionPreview
  259. *
  260. * @param Element\ElementInterface[]|null $data
  261. * @param null|DataObject\Concrete $object
  262. * @param mixed $params
  263. *
  264. * @return string|null
  265. */
  266. public function getVersionPreview($data, $object = null, $params = [])
  267. {
  268. if (is_array($data) && count($data) > 0) {
  269. $paths = [];
  270. foreach ($data as $o) {
  271. if ($o instanceof Element\ElementInterface) {
  272. $paths[] = $o->getRealFullPath();
  273. }
  274. }
  275. return implode('<br />', $paths);
  276. }
  277. return null;
  278. }
  279. /**
  280. * @return string|int
  281. */
  282. public function getWidth()
  283. {
  284. return $this->width;
  285. }
  286. /**
  287. * @param string|int $width
  288. *
  289. * @return $this
  290. */
  291. public function setWidth($width)
  292. {
  293. if (is_numeric($width)) {
  294. $width = (int)$width;
  295. }
  296. $this->width = $width;
  297. return $this;
  298. }
  299. /**
  300. * @return string|int
  301. */
  302. public function getHeight()
  303. {
  304. return $this->height;
  305. }
  306. /**
  307. * @param string|int $height
  308. *
  309. * @return $this
  310. */
  311. public function setHeight($height)
  312. {
  313. if (is_numeric($height)) {
  314. $height = (int)$height;
  315. }
  316. $this->height = $height;
  317. return $this;
  318. }
  319. /**
  320. * {@inheritdoc}
  321. */
  322. public function checkValidity($data, $omitMandatoryCheck = false, $params = [])
  323. {
  324. if (!$omitMandatoryCheck && $this->getMandatory() && empty($data)) {
  325. throw new Element\ValidationException('Empty mandatory field [ '.$this->getName().' ]');
  326. }
  327. if (is_array($data)) {
  328. $this->performMultipleAssignmentCheck($data);
  329. foreach ($data as $o) {
  330. if (empty($o)) {
  331. continue;
  332. }
  333. $allowClass = $this->allowObjectRelation($o);
  334. if (!$allowClass || !($o instanceof DataObject\Concrete)) {
  335. if (!$allowClass && $o instanceof DataObject\Concrete) {
  336. $id = $o->getId();
  337. } else {
  338. $id = '??';
  339. }
  340. throw new Element\ValidationException('Invalid object relation to object ['.$id.'] in field ' . $this->getName(). ' , tried to assign ' . $o->getId(), null, null);
  341. }
  342. }
  343. if ($this->getMaxItems() && count($data) > $this->getMaxItems()) {
  344. throw new Element\ValidationException('Number of allowed relations in field `' . $this->getName() . '` exceeded (max. ' . $this->getMaxItems() . ')');
  345. }
  346. }
  347. }
  348. /**
  349. * {@inheritdoc}
  350. */
  351. public function getForCsvExport($object, $params = [])
  352. {
  353. $data = $this->getDataFromObjectParam($object, $params);
  354. if (is_array($data)) {
  355. $paths = [];
  356. foreach ($data as $eo) {
  357. if ($eo instanceof Element\ElementInterface) {
  358. $paths[] = $eo->getRealFullPath();
  359. }
  360. }
  361. return implode(',', $paths);
  362. }
  363. return '';
  364. }
  365. /**
  366. * @param DataObject\AbstractObject[]|null $data
  367. *
  368. * @return array
  369. */
  370. public function resolveDependencies($data)
  371. {
  372. $dependencies = [];
  373. if (is_array($data) && count($data) > 0) {
  374. foreach ($data as $o) {
  375. if ($o instanceof DataObject\AbstractObject) {
  376. $dependencies['object_' . $o->getId()] = [
  377. 'id' => $o->getId(),
  378. 'type' => 'object',
  379. ];
  380. }
  381. }
  382. }
  383. return $dependencies;
  384. }
  385. /**
  386. * @param DataObject\Concrete|DataObject\Localizedfield|DataObject\Objectbrick\Data\AbstractData|DataObject\Fieldcollection\Data\AbstractData $container
  387. * @param array $params
  388. *
  389. * @return array
  390. */
  391. public function preGetData(/** mixed */ $container, /** array */ $params = []) // : mixed
  392. {
  393. $data = null;
  394. if ($container instanceof DataObject\Concrete) {
  395. $data = $container->getObjectVar($this->getName());
  396. if (!$container->isLazyKeyLoaded($this->getName())) {
  397. $data = $this->load($container);
  398. $container->setObjectVar($this->getName(), $data);
  399. $this->markLazyloadedFieldAsLoaded($container);
  400. }
  401. } elseif ($container instanceof DataObject\Localizedfield) {
  402. $data = $params['data'];
  403. } elseif ($container instanceof DataObject\Fieldcollection\Data\AbstractData) {
  404. parent::loadLazyFieldcollectionField($container);
  405. $data = $container->getObjectVar($this->getName());
  406. } elseif ($container instanceof DataObject\Objectbrick\Data\AbstractData) {
  407. parent::loadLazyBrickField($container);
  408. $data = $container->getObjectVar($this->getName());
  409. }
  410. if (DataObject::doHideUnpublished() && is_array($data)) {
  411. $publishedList = [];
  412. foreach ($data as $listElement) {
  413. if (Element\Service::isPublished($listElement)) {
  414. $publishedList[] = $listElement;
  415. }
  416. }
  417. return $publishedList;
  418. }
  419. return is_array($data) ? $data : [];
  420. }
  421. /**
  422. * { @inheritdoc }
  423. */
  424. public function preSetData(/** mixed */ $container, /** mixed */ $data, /** array */ $params = []) // : mixed
  425. {
  426. if ($data === null) {
  427. $data = [];
  428. }
  429. $this->markLazyloadedFieldAsLoaded($container);
  430. return $data;
  431. }
  432. /**
  433. * @param int|string|null $maxItems
  434. *
  435. * @return $this
  436. */
  437. public function setMaxItems($maxItems)
  438. {
  439. $this->maxItems = $this->getAsIntegerCast($maxItems);
  440. return $this;
  441. }
  442. /**
  443. * @return int
  444. */
  445. public function getMaxItems()
  446. {
  447. return $this->maxItems;
  448. }
  449. /**
  450. * {@inheritdoc}
  451. */
  452. public function isDiffChangeAllowed($object, $params = [])
  453. {
  454. return true;
  455. }
  456. /** Generates a pretty version preview (similar to getVersionPreview) can be either html or
  457. * a image URL. See the https://github.com/pimcore/object-merger bundle documentation for details
  458. *
  459. * @param Element\ElementInterface[]|null $data
  460. * @param DataObject\Concrete|null $object
  461. * @param mixed $params
  462. *
  463. * @return array|string
  464. */
  465. public function getDiffVersionPreview($data, $object = null, $params = [])
  466. {
  467. $value = [];
  468. $value['type'] = 'html';
  469. $value['html'] = '';
  470. if ($data) {
  471. $html = $this->getVersionPreview($data, $object, $params);
  472. $value['html'] = $html;
  473. }
  474. return $value;
  475. }
  476. /**
  477. * { @inheritdoc }
  478. */
  479. public function rewriteIds(/** mixed */ $container, /** array */ $idMapping, /** array */ $params = []) /** :mixed */
  480. {
  481. $data = $this->getDataFromObjectParam($container, $params);
  482. $data = $this->rewriteIdsService($data, $idMapping);
  483. return $data;
  484. }
  485. /**
  486. * @param DataObject\ClassDefinition\Data\ManyToManyObjectRelation $masterDefinition
  487. */
  488. public function synchronizeWithMasterDefinition(DataObject\ClassDefinition\Data $masterDefinition)
  489. {
  490. $this->maxItems = $masterDefinition->maxItems;
  491. $this->relationType = $masterDefinition->relationType;
  492. }
  493. /**
  494. * {@inheritdoc}
  495. */
  496. public function enrichLayoutDefinition(/*?Concrete */ $object, /** array */ $context = []) // : self
  497. {
  498. if (!$this->visibleFields) {
  499. return $this;
  500. }
  501. $classIds = $this->getClasses();
  502. if (empty($classIds[0]['classes'])) {
  503. return $this;
  504. }
  505. $classId = $classIds[0]['classes'];
  506. if (is_numeric($classId)) {
  507. $class = DataObject\ClassDefinition::getById($classId);
  508. } else {
  509. $class = DataObject\ClassDefinition::getByName($classId);
  510. }
  511. if (!$class) {
  512. return $this;
  513. }
  514. $this->visibleFieldDefinitions = [];
  515. $translator = \Pimcore::getContainer()->get('translator');
  516. $visibleFields = explode(',', $this->visibleFields);
  517. foreach ($visibleFields as $field) {
  518. $fd = $class->getFieldDefinition($field, $context);
  519. if (!$fd) {
  520. $fieldFound = false;
  521. /** @var Localizedfields|null $localizedfields */
  522. $localizedfields = $class->getFieldDefinitions($context)['localizedfields'] ?? null;
  523. if ($localizedfields) {
  524. if ($fd = $localizedfields->getFieldDefinition($field)) {
  525. $this->visibleFieldDefinitions[$field]['name'] = $fd->getName();
  526. $this->visibleFieldDefinitions[$field]['title'] = $fd->getTitle();
  527. $this->visibleFieldDefinitions[$field]['fieldtype'] = $fd->getFieldType();
  528. if ($fd instanceof DataObject\ClassDefinition\Data\Select || $fd instanceof DataObject\ClassDefinition\Data\Multiselect) {
  529. $this->visibleFieldDefinitions[$field]['options'] = $fd->getOptions();
  530. }
  531. $fieldFound = true;
  532. }
  533. }
  534. if (!$fieldFound) {
  535. $this->visibleFieldDefinitions[$field]['name'] = $field;
  536. $this->visibleFieldDefinitions[$field]['title'] = $translator->trans($field, [], 'admin');
  537. $this->visibleFieldDefinitions[$field]['fieldtype'] = 'input';
  538. }
  539. } else {
  540. $this->visibleFieldDefinitions[$field]['name'] = $fd->getName();
  541. $this->visibleFieldDefinitions[$field]['title'] = $fd->getTitle();
  542. $this->visibleFieldDefinitions[$field]['fieldtype'] = $fd->getFieldType();
  543. $this->visibleFieldDefinitions[$field]['noteditable'] = true;
  544. if ($fd instanceof DataObject\ClassDefinition\Data\Select || $fd instanceof DataObject\ClassDefinition\Data\Multiselect) {
  545. if ($fd->getOptionsProviderClass()) {
  546. $this->visibleFieldDefinitions[$field]['optionsProviderClass'] = $fd->getOptionsProviderClass();
  547. }
  548. $this->visibleFieldDefinitions[$field]['options'] = $fd->getOptions();
  549. }
  550. }
  551. }
  552. return $this;
  553. }
  554. /**
  555. * {@inheritdoc}
  556. */
  557. protected function getPhpdocType()
  558. {
  559. return implode(' | ', $this->getPhpDocClassString(true));
  560. }
  561. /**
  562. * {@inheritdoc}
  563. */
  564. public function normalize($value, $params = [])
  565. {
  566. if (is_array($value)) {
  567. $result = [];
  568. foreach ($value as $element) {
  569. $type = Element\Service::getType($element);
  570. $id = $element->getId();
  571. $result[] = [
  572. 'type' => $type,
  573. 'id' => $id,
  574. ];
  575. }
  576. return $result;
  577. }
  578. return null;
  579. }
  580. /**
  581. * {@inheritdoc}
  582. */
  583. public function denormalize($value, $params = [])
  584. {
  585. if (is_array($value)) {
  586. $result = [];
  587. foreach ($value as $elementData) {
  588. $type = $elementData['type'];
  589. $id = $elementData['id'];
  590. $element = Element\Service::getElementById($type, $id);
  591. if ($element) {
  592. $result[] = $element;
  593. }
  594. }
  595. return $result;
  596. }
  597. return null;
  598. }
  599. /**
  600. * Returns a ID which must be unique across the grid rows
  601. *
  602. * @internal
  603. *
  604. * @param array $item
  605. *
  606. * @return string
  607. */
  608. protected function buildUniqueKeyForDiffEditor($item)
  609. {
  610. return $item['id'];
  611. }
  612. /**
  613. * @internal
  614. */
  615. protected function processDiffDataForEditMode($originalData, $data, $object = null, $params = [])
  616. {
  617. if ($data) {
  618. $data = $data[0];
  619. $items = $data['data'];
  620. $newItems = [];
  621. if ($items) {
  622. foreach ($items as $in) {
  623. $item = [];
  624. $item['id'] = $in['id'];
  625. $item['path'] = $in['fullpath'];
  626. $item['type'] = $in['type'];
  627. $unique = $this->buildUniqueKeyForDiffEditor($item);
  628. $itemId = json_encode($item);
  629. $raw = $itemId;
  630. $newItems[] = [
  631. 'itemId' => $itemId,
  632. 'title' => $item['path'],
  633. 'raw' => $raw,
  634. 'gridrow' => $item,
  635. 'unique' => $unique,
  636. ];
  637. }
  638. $data['data'] = $newItems;
  639. }
  640. $data['value'] = [
  641. 'type' => 'grid',
  642. 'columnConfig' => [
  643. 'id' => [
  644. 'width' => 60,
  645. ],
  646. 'path' => [
  647. 'flex' => 2,
  648. ],
  649. ],
  650. 'html' => $this->getVersionPreview($originalData, $object, $params),
  651. ];
  652. $newData = [];
  653. $newData[] = $data;
  654. return $newData;
  655. }
  656. return $data;
  657. }
  658. /**
  659. * {@inheritdoc}
  660. */
  661. public function getDiffDataForEditMode($data, $object = null, $params = [])
  662. {
  663. $originalData = $data;
  664. $data = parent::getDiffDataForEditMode($data, $object, $params);
  665. $data = $this->processDiffDataForEditMode($originalData, $data, $object, $params);
  666. return $data;
  667. }
  668. /** See parent class.
  669. * @param array $data
  670. * @param DataObject\Concrete|null $object
  671. * @param mixed $params
  672. *
  673. * @return mixed
  674. */
  675. public function getDiffDataFromEditmode($data, $object = null, $params = [])
  676. {
  677. if ($data) {
  678. $tabledata = $data[0]['data'];
  679. $result = [];
  680. if ($tabledata) {
  681. foreach ($tabledata as $in) {
  682. $out = json_decode($in['raw'], true);
  683. $result[] = $out;
  684. }
  685. }
  686. return $this->getDataFromEditmode($result, $object, $params);
  687. }
  688. return;
  689. }
  690. /**
  691. * @param array|string|null $visibleFields
  692. *
  693. * @return $this
  694. */
  695. public function setVisibleFields($visibleFields)
  696. {
  697. if (is_array($visibleFields) && count($visibleFields)) {
  698. $visibleFields = implode(',', $visibleFields);
  699. }
  700. $this->visibleFields = $visibleFields;
  701. return $this;
  702. }
  703. /**
  704. * @return string|null
  705. */
  706. public function getVisibleFields()
  707. {
  708. return $this->visibleFields;
  709. }
  710. /**
  711. * @return bool
  712. */
  713. public function isAllowToCreateNewObject(): bool
  714. {
  715. return $this->allowToCreateNewObject;
  716. }
  717. /**
  718. * @param bool $allowToCreateNewObject
  719. */
  720. public function setAllowToCreateNewObject($allowToCreateNewObject)
  721. {
  722. $this->allowToCreateNewObject = (bool)$allowToCreateNewObject;
  723. }
  724. /**
  725. * {@inheritdoc}
  726. */
  727. public function isOptimizedAdminLoading(): bool
  728. {
  729. return (bool) $this->optimizedAdminLoading;
  730. }
  731. /**
  732. * @param bool $optimizedAdminLoading
  733. */
  734. public function setOptimizedAdminLoading($optimizedAdminLoading)
  735. {
  736. $this->optimizedAdminLoading = $optimizedAdminLoading;
  737. }
  738. /**
  739. * @return bool
  740. */
  741. public function isEnableTextSelection(): bool
  742. {
  743. return $this->enableTextSelection;
  744. }
  745. /**
  746. * @param bool $enableTextSelection
  747. */
  748. public function setEnableTextSelection(bool $enableTextSelection): void
  749. {
  750. $this->enableTextSelection = $enableTextSelection;
  751. }
  752. /**
  753. * {@inheritdoc}
  754. */
  755. public function isFilterable(): bool
  756. {
  757. return true;
  758. }
  759. /**
  760. * {@inheritdoc}
  761. */
  762. public function addListingFilter(DataObject\Listing $listing, $data, $operator = '=')
  763. {
  764. if ($data instanceof DataObject\Concrete) {
  765. $data = $data->getId();
  766. }
  767. if ($operator === '=') {
  768. $listing->addConditionParam('`'.$this->getName().'` LIKE ?', '%,'.$data.',%');
  769. return $listing;
  770. }
  771. return parent::addListingFilter($listing, $data, $operator);
  772. }
  773. }