vendor/pimcore/pimcore/models/DataObject/AbstractObject.php line 385

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;
  15. use Doctrine\DBAL\Exception\RetryableException;
  16. use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
  17. use Pimcore\Cache;
  18. use Pimcore\Cache\RuntimeCache;
  19. use Pimcore\Db;
  20. use Pimcore\Event\DataObjectEvents;
  21. use Pimcore\Event\Model\DataObjectEvent;
  22. use Pimcore\Logger;
  23. use Pimcore\Model;
  24. use Pimcore\Model\DataObject;
  25. use Pimcore\Model\Element;
  26. use Pimcore\Model\Element\DuplicateFullPathException;
  27. /**
  28. * @method AbstractObject\Dao getDao()
  29. * @method array|null getPermissions(?string $type, Model\User $user, bool $quote = true)
  30. * @method bool __isBasedOnLatestData()
  31. * @method string getCurrentFullPath()
  32. * @method int getChildAmount($objectTypes = [DataObject::OBJECT_TYPE_OBJECT, DataObject::OBJECT_TYPE_FOLDER], Model\User $user = null)
  33. * @method array getChildPermissions(?string $type, Model\User $user, bool $quote = true)
  34. */
  35. abstract class AbstractObject extends Model\Element\AbstractElement
  36. {
  37. const OBJECT_TYPE_FOLDER = 'folder';
  38. const OBJECT_TYPE_OBJECT = 'object';
  39. const OBJECT_TYPE_VARIANT = 'variant';
  40. const OBJECT_CHILDREN_SORT_BY_DEFAULT = 'key';
  41. const OBJECT_CHILDREN_SORT_BY_INDEX = 'index';
  42. const OBJECT_CHILDREN_SORT_ORDER_DEFAULT = 'ASC';
  43. /**
  44. * possible types of a document
  45. *
  46. * @var array
  47. */
  48. public static $types = [self::OBJECT_TYPE_FOLDER, self::OBJECT_TYPE_OBJECT, self::OBJECT_TYPE_VARIANT];
  49. /**
  50. * @var bool
  51. */
  52. private static $hideUnpublished = false;
  53. /**
  54. * @var bool
  55. */
  56. private static $getInheritedValues = false;
  57. /**
  58. * @internal
  59. *
  60. * @var bool
  61. */
  62. protected static $disableDirtyDetection = false;
  63. /**
  64. * @internal
  65. *
  66. * @var string[]
  67. */
  68. protected static $objectColumns = ['o_id', 'o_parentid', 'o_type', 'o_key', 'o_classid', 'o_classname', 'o_path'];
  69. /**
  70. * @internal
  71. *
  72. * @deprecated
  73. *
  74. * @var int|null
  75. */
  76. protected $o_id;
  77. /**
  78. * @internal
  79. *
  80. * @deprecated
  81. *
  82. * @var int|null
  83. */
  84. protected $o_parentId;
  85. /**
  86. * @internal
  87. *
  88. * @deprecated
  89. */
  90. protected $o_parent;
  91. /**
  92. * @internal
  93. *
  94. * @var string
  95. */
  96. protected $o_type = 'object';
  97. /**
  98. * @internal
  99. *
  100. * @var string|null
  101. */
  102. protected $o_key;
  103. /**
  104. * @internal
  105. *
  106. * @deprecated
  107. *
  108. * @var string|null
  109. */
  110. protected $o_path;
  111. /**
  112. * @internal
  113. *
  114. * @var int
  115. */
  116. protected $o_index = 0;
  117. /**
  118. * @internal
  119. *
  120. * @deprecated
  121. *
  122. * @var int|null
  123. */
  124. protected $o_creationDate;
  125. /**
  126. * @internal
  127. *
  128. * @deprecated
  129. *
  130. * @var int|null
  131. */
  132. protected $o_modificationDate;
  133. /**
  134. * @internal
  135. *
  136. * @deprecated
  137. *
  138. * @var int|null
  139. */
  140. protected ?int $o_userOwner = null;
  141. /**
  142. * @internal
  143. *
  144. * @deprecated
  145. *
  146. * @var int|null
  147. */
  148. protected ?int $o_userModification = null;
  149. /**
  150. * @internal
  151. *
  152. * @var bool[]
  153. */
  154. protected $o_hasChildren = [];
  155. /**
  156. * Contains a list of sibling documents
  157. *
  158. * @internal
  159. *
  160. * @var array
  161. */
  162. protected $o_siblings = [];
  163. /**
  164. * Indicator if object has siblings or not
  165. *
  166. * @internal
  167. *
  168. * @var bool[]
  169. */
  170. protected $o_hasSiblings = [];
  171. /**
  172. * @internal
  173. *
  174. * @var array
  175. */
  176. protected $o_children = [];
  177. /**
  178. * @internal
  179. *
  180. * @deprecated
  181. *
  182. * @var string
  183. */
  184. protected $o_locked;
  185. /**
  186. * @internal
  187. *
  188. * @var string|null
  189. */
  190. protected $o_childrenSortBy;
  191. /**
  192. * @internal
  193. *
  194. * @var string|null
  195. */
  196. protected $o_childrenSortOrder;
  197. /**
  198. * @internal
  199. *
  200. * @deprecated
  201. *
  202. * @var int
  203. */
  204. protected $o_versionCount = 0;
  205. /**
  206. * @internal
  207. *
  208. * @deprecated
  209. *
  210. * @var array|null
  211. */
  212. protected $o_properties = null;
  213. public function __construct()
  214. {
  215. $this->o_id = & $this->id;
  216. $this->o_path = & $this->path;
  217. $this->o_creationDate = & $this->creationDate;
  218. $this->o_userOwner = & $this->userOwner;
  219. $this->o_versionCount = & $this->versionCount;
  220. $this->o_modificationDate = & $this->modificationDate;
  221. $this->o_locked = & $this->locked;
  222. $this->o_parent = & $this->parent;
  223. $this->o_properties = & $this->properties;
  224. $this->o_userModification = & $this->userModification;
  225. $this->o_parentId = & $this->parentId;
  226. }
  227. /**
  228. * {@inheritdoc}
  229. */
  230. protected function getBlockedVars(): array
  231. {
  232. $blockedVars = ['o_hasChildren', 'o_versions', 'o_class', 'scheduledTasks', 'o_parent', 'parent', 'omitMandatoryCheck'];
  233. if ($this->isInDumpState()) {
  234. // this is if we want to make a full dump of the object (eg. for a new version), including children for recyclebin
  235. $blockedVars = array_merge($blockedVars, ['o_dirtyFields']);
  236. } else {
  237. // this is if we want to cache the object
  238. $blockedVars = array_merge($blockedVars, ['o_children', 'properties', 'o_properties']);
  239. }
  240. return $blockedVars;
  241. }
  242. /**
  243. * @static
  244. *
  245. * @return bool
  246. */
  247. public static function getHideUnpublished()
  248. {
  249. return self::$hideUnpublished;
  250. }
  251. /**
  252. * @static
  253. *
  254. * @param bool $hideUnpublished
  255. */
  256. public static function setHideUnpublished($hideUnpublished)
  257. {
  258. self::$hideUnpublished = $hideUnpublished;
  259. }
  260. /**
  261. * @static
  262. *
  263. * @return bool
  264. */
  265. public static function doHideUnpublished()
  266. {
  267. return self::$hideUnpublished;
  268. }
  269. /**
  270. * @static
  271. *
  272. * @param bool $getInheritedValues
  273. */
  274. public static function setGetInheritedValues($getInheritedValues)
  275. {
  276. self::$getInheritedValues = $getInheritedValues;
  277. }
  278. /**
  279. * @static
  280. *
  281. * @return bool
  282. */
  283. public static function getGetInheritedValues()
  284. {
  285. return self::$getInheritedValues;
  286. }
  287. /**
  288. * @static
  289. *
  290. * @param Concrete|null $object
  291. *
  292. * @return bool
  293. */
  294. public static function doGetInheritedValues(Concrete $object = null)
  295. {
  296. if (self::$getInheritedValues && $object !== null) {
  297. $class = $object->getClass();
  298. return $class->getAllowInherit();
  299. }
  300. return self::$getInheritedValues;
  301. }
  302. /**
  303. * get possible types
  304. *
  305. * @return array
  306. */
  307. public static function getTypes()
  308. {
  309. return self::$types;
  310. }
  311. /**
  312. * Static helper to get an object by the passed ID
  313. *
  314. * @param int|string $id
  315. * @param array|bool $force
  316. *
  317. * @return static|null
  318. */
  319. public static function getById($id, $force = false)
  320. {
  321. if (!is_numeric($id) || $id < 1) {
  322. return null;
  323. }
  324. $id = (int)$id;
  325. $cacheKey = self::getCacheKey($id);
  326. $params = Model\Element\Service::prepareGetByIdParams($force, __METHOD__, func_num_args() > 1);
  327. if (!$params['force'] && RuntimeCache::isRegistered($cacheKey)) {
  328. $object = RuntimeCache::get($cacheKey);
  329. if ($object && static::typeMatch($object)) {
  330. return $object;
  331. }
  332. }
  333. if ($params['force'] || !($object = Cache::load($cacheKey))) {
  334. $object = new Model\DataObject();
  335. try {
  336. $typeInfo = $object->getDao()->getTypeById($id);
  337. if (!empty($typeInfo['o_type']) && in_array($typeInfo['o_type'], DataObject::$types)) {
  338. if ($typeInfo['o_type'] == DataObject::OBJECT_TYPE_FOLDER) {
  339. $className = Folder::class;
  340. } else {
  341. $className = 'Pimcore\\Model\\DataObject\\' . ucfirst($typeInfo['o_className']);
  342. }
  343. /** @var AbstractObject $object */
  344. $object = self::getModelFactory()->build($className);
  345. RuntimeCache::set($cacheKey, $object);
  346. $object->getDao()->getById($id);
  347. $object->__setDataVersionTimestamp($object->getModificationDate());
  348. Service::recursiveResetDirtyMap($object);
  349. // force loading of relation data
  350. if ($object instanceof Concrete) {
  351. $object->__getRawRelationData();
  352. }
  353. Cache::save($object, $cacheKey);
  354. } else {
  355. throw new Model\Exception\NotFoundException('No entry for object id ' . $id);
  356. }
  357. } catch (Model\Exception\NotFoundException $e) {
  358. return null;
  359. }
  360. } else {
  361. RuntimeCache::set($cacheKey, $object);
  362. }
  363. if (!$object || !static::typeMatch($object)) {
  364. return null;
  365. }
  366. \Pimcore::getEventDispatcher()->dispatch(
  367. new DataObjectEvent($object, ['params' => $params]),
  368. DataObjectEvents::POST_LOAD
  369. );
  370. return $object;
  371. }
  372. /**
  373. * @param string $path
  374. * @param array|bool $force
  375. *
  376. * @return static|null
  377. */
  378. public static function getByPath($path, $force = false)
  379. {
  380. if (!$path) {
  381. return null;
  382. }
  383. $path = Model\Element\Service::correctPath($path);
  384. try {
  385. $object = new static();
  386. $object->getDao()->getByPath($path);
  387. return static::getById($object->getId(), Model\Element\Service::prepareGetByIdParams($force, __METHOD__, func_num_args() > 1));
  388. } catch (Model\Exception\NotFoundException $e) {
  389. return null;
  390. }
  391. }
  392. /**
  393. * @param array $config
  394. *
  395. * @return DataObject\Listing
  396. *
  397. * @throws \Exception
  398. */
  399. public static function getList($config = [])
  400. {
  401. $className = DataObject::class;
  402. // get classname
  403. if (!in_array(static::class, [__CLASS__, Concrete::class, Folder::class], true)) {
  404. /** @var Concrete $tmpObject */
  405. $tmpObject = new static();
  406. if ($tmpObject instanceof Concrete) {
  407. $className = 'Pimcore\\Model\\DataObject\\' . ucfirst($tmpObject->getClassName());
  408. }
  409. }
  410. if (is_array($config)) {
  411. if (!empty($config['class'])) {
  412. $className = ltrim($config['class'], '\\');
  413. }
  414. if ($className) {
  415. $listClass = $className . '\\Listing';
  416. /** @var DataObject\Listing $list */
  417. $list = self::getModelFactory()->build($listClass);
  418. $list->setValues($config);
  419. return $list;
  420. }
  421. }
  422. throw new \Exception('Unable to initiate list class - class not found or invalid configuration');
  423. }
  424. /**
  425. * @deprecated will be removed in Pimcore 11
  426. *
  427. * @param array $config
  428. *
  429. * @return int total count
  430. */
  431. public static function getTotalCount($config = [])
  432. {
  433. $list = static::getList($config);
  434. $count = $list->getTotalCount();
  435. return $count;
  436. }
  437. /**
  438. * @internal
  439. *
  440. * @param AbstractObject $object
  441. *
  442. * @return bool
  443. */
  444. protected static function typeMatch(AbstractObject $object)
  445. {
  446. if (static::class === Concrete::class && !$object instanceof static) {
  447. trigger_deprecation(
  448. 'pimcore/pimcore',
  449. '10.5',
  450. 'Loading non-Concrete objects with the Concrete class will not be possible in Pimcore 11'
  451. );
  452. return true;
  453. }
  454. return static::class === self::class || $object instanceof static;
  455. }
  456. /**
  457. * @param array $objectTypes
  458. * @param bool $includingUnpublished
  459. *
  460. * @return DataObject[]
  461. */
  462. public function getChildren(array $objectTypes = [self::OBJECT_TYPE_OBJECT, self::OBJECT_TYPE_FOLDER], $includingUnpublished = false)
  463. {
  464. $cacheKey = $this->getListingCacheKey(func_get_args());
  465. if (!isset($this->o_children[$cacheKey])) {
  466. if ($this->getId()) {
  467. $list = new Listing();
  468. $list->setUnpublished($includingUnpublished);
  469. $list->setCondition('o_parentId = ?', $this->getId());
  470. $list->setOrderKey(sprintf('o_%s', $this->getChildrenSortBy()));
  471. $list->setOrder($this->getChildrenSortOrder());
  472. $list->setObjectTypes($objectTypes);
  473. $this->o_children[$cacheKey] = $list->load();
  474. $this->o_hasChildren[$cacheKey] = (bool) count($this->o_children[$cacheKey]);
  475. } else {
  476. $this->o_children[$cacheKey] = [];
  477. $this->o_hasChildren[$cacheKey] = false;
  478. }
  479. }
  480. return $this->o_children[$cacheKey];
  481. }
  482. /**
  483. * Quick test if there are children
  484. *
  485. * @param array $objectTypes
  486. * @param bool|null $includingUnpublished
  487. *
  488. * @return bool
  489. */
  490. public function hasChildren($objectTypes = [self::OBJECT_TYPE_OBJECT, self::OBJECT_TYPE_FOLDER], $includingUnpublished = null)
  491. {
  492. $cacheKey = $this->getListingCacheKey(func_get_args());
  493. if (isset($this->o_hasChildren[$cacheKey])) {
  494. return $this->o_hasChildren[$cacheKey];
  495. }
  496. return $this->o_hasChildren[$cacheKey] = $this->getDao()->hasChildren($objectTypes, $includingUnpublished);
  497. }
  498. /**
  499. * Get a list of the sibling documents
  500. *
  501. * @param array $objectTypes
  502. * @param bool $includingUnpublished
  503. *
  504. * @return array
  505. */
  506. public function getSiblings(array $objectTypes = [self::OBJECT_TYPE_OBJECT, self::OBJECT_TYPE_FOLDER], $includingUnpublished = false)
  507. {
  508. $cacheKey = $this->getListingCacheKey(func_get_args());
  509. if (!isset($this->o_siblings[$cacheKey])) {
  510. if ($this->getParentId()) {
  511. $list = new Listing();
  512. $list->setUnpublished($includingUnpublished);
  513. $list->addConditionParam('o_parentId = ?', $this->getParentId());
  514. if ($this->getId()) {
  515. $list->addConditionParam('o_id != ?', $this->getId());
  516. }
  517. $list->setOrderKey('o_key');
  518. $list->setObjectTypes($objectTypes);
  519. $list->setOrder('asc');
  520. $this->o_siblings[$cacheKey] = $list->load();
  521. $this->o_hasSiblings[$cacheKey] = (bool) count($this->o_siblings[$cacheKey]);
  522. } else {
  523. $this->o_siblings[$cacheKey] = [];
  524. $this->o_hasSiblings[$cacheKey] = false;
  525. }
  526. }
  527. return $this->o_siblings[$cacheKey];
  528. }
  529. /**
  530. * Returns true if the object has at least one sibling
  531. *
  532. * @param array $objectTypes
  533. * @param bool|null $includingUnpublished
  534. *
  535. * @return bool
  536. */
  537. public function hasSiblings($objectTypes = [self::OBJECT_TYPE_OBJECT, self::OBJECT_TYPE_FOLDER], $includingUnpublished = null)
  538. {
  539. $cacheKey = $this->getListingCacheKey(func_get_args());
  540. if (isset($this->o_hasSiblings[$cacheKey])) {
  541. return $this->o_hasSiblings[$cacheKey];
  542. }
  543. return $this->o_hasSiblings[$cacheKey] = $this->getDao()->hasSiblings($objectTypes, $includingUnpublished);
  544. }
  545. /**
  546. * @internal
  547. *
  548. * @throws \Exception
  549. */
  550. protected function doDelete()
  551. {
  552. // delete children
  553. $children = $this->getChildren(self::$types, true);
  554. if (count($children) > 0) {
  555. foreach ($children as $child) {
  556. $child->delete();
  557. }
  558. }
  559. // remove dependencies
  560. $d = new Model\Dependency;
  561. $d->cleanAllForElement($this);
  562. // remove all properties
  563. $this->getDao()->deleteAllProperties();
  564. }
  565. /**
  566. * @throws \Exception
  567. */
  568. public function delete()
  569. {
  570. $this->dispatchEvent(new DataObjectEvent($this), DataObjectEvents::PRE_DELETE);
  571. $this->beginTransaction();
  572. try {
  573. $this->doDelete();
  574. $this->getDao()->delete();
  575. $this->commit();
  576. //clear parent data from registry
  577. $parentCacheKey = self::getCacheKey($this->getParentId());
  578. if (RuntimeCache::isRegistered($parentCacheKey)) {
  579. /** @var AbstractObject $parent * */
  580. $parent = RuntimeCache::get($parentCacheKey);
  581. if ($parent instanceof self) {
  582. $parent->setChildren(null);
  583. }
  584. }
  585. } catch (\Exception $e) {
  586. try {
  587. $this->rollBack();
  588. } catch (\Exception $er) {
  589. // PDO adapter throws exceptions if rollback fails
  590. Logger::info((string) $er);
  591. }
  592. $failureEvent = new DataObjectEvent($this);
  593. $failureEvent->setArgument('exception', $e);
  594. $this->dispatchEvent($failureEvent, DataObjectEvents::POST_DELETE_FAILURE);
  595. Logger::crit((string) $e);
  596. throw $e;
  597. }
  598. // empty object cache
  599. $this->clearDependentCache();
  600. //clear object from registry
  601. RuntimeCache::set(self::getCacheKey($this->getId()), null);
  602. $this->dispatchEvent(new DataObjectEvent($this), DataObjectEvents::POST_DELETE);
  603. }
  604. /**
  605. * @return $this
  606. *
  607. * @throws \Exception
  608. */
  609. public function save()
  610. {
  611. // additional parameters (e.g. "versionNote" for the version note)
  612. $params = [];
  613. if (func_num_args() && is_array(func_get_arg(0))) {
  614. $params = func_get_arg(0);
  615. }
  616. $isUpdate = false;
  617. $differentOldPath = null;
  618. try {
  619. $isDirtyDetectionDisabled = self::isDirtyDetectionDisabled();
  620. $preEvent = new DataObjectEvent($this, $params);
  621. if ($this->getId()) {
  622. $isUpdate = true;
  623. $this->dispatchEvent($preEvent, DataObjectEvents::PRE_UPDATE);
  624. } else {
  625. self::disableDirtyDetection();
  626. $this->dispatchEvent($preEvent, DataObjectEvents::PRE_ADD);
  627. }
  628. $params = $preEvent->getArguments();
  629. $this->correctPath();
  630. // we wrap the save actions in a loop here, so that we can restart the database transactions in the case it fails
  631. // if a transaction fails it gets restarted $maxRetries times, then the exception is thrown out
  632. // this is especially useful to avoid problems with deadlocks in multi-threaded environments (forked workers, ...)
  633. $maxRetries = 5;
  634. for ($retries = 0; $retries < $maxRetries; $retries++) {
  635. // be sure that unpublished objects in relations are saved also in frontend mode, eg. in importers, ...
  636. $hideUnpublishedBackup = self::getHideUnpublished();
  637. self::setHideUnpublished(false);
  638. $this->beginTransaction();
  639. try {
  640. if (!in_array($this->getType(), self::$types)) {
  641. throw new \Exception('invalid object type given: [' . $this->getType() . ']');
  642. }
  643. if (!$isUpdate) {
  644. $this->getDao()->create();
  645. }
  646. // get the old path from the database before the update is done
  647. $oldPath = null;
  648. if ($isUpdate) {
  649. $oldPath = $this->getDao()->getCurrentFullPath();
  650. }
  651. // if the old path is different from the new path, update all children
  652. // we need to do the update of the children's path before $this->update() because the
  653. // inheritance helper needs the correct paths of the children in InheritanceHelper::buildTree()
  654. $updatedChildren = [];
  655. if ($oldPath && $oldPath != $this->getRealFullPath()) {
  656. $differentOldPath = $oldPath;
  657. $this->getDao()->updateWorkspaces();
  658. $updatedChildren = $this->getDao()->updateChildPaths($oldPath);
  659. }
  660. $this->update($isUpdate, $params);
  661. self::setHideUnpublished($hideUnpublishedBackup);
  662. $this->commit();
  663. break; // transaction was successfully completed, so we cancel the loop here -> no restart required
  664. } catch (\Exception $e) {
  665. try {
  666. $this->rollBack();
  667. } catch (\Exception $er) {
  668. // PDO adapter throws exceptions if rollback fails
  669. Logger::info((string) $er);
  670. }
  671. // set "HideUnpublished" back to the value it was originally
  672. self::setHideUnpublished($hideUnpublishedBackup);
  673. if ($e instanceof UniqueConstraintViolationException) {
  674. throw new Element\ValidationException('unique constraint violation', 0, $e);
  675. }
  676. if ($e instanceof RetryableException) {
  677. // we try to start the transaction $maxRetries times again (deadlocks, ...)
  678. if ($retries < ($maxRetries - 1)) {
  679. $run = $retries + 1;
  680. $waitTime = random_int(1, 5) * 100000; // microseconds
  681. Logger::warn('Unable to finish transaction (' . $run . ". run) because of the following reason '" . $e->getMessage() . "'. --> Retrying in " . $waitTime . ' microseconds ... (' . ($run + 1) . ' of ' . $maxRetries . ')');
  682. usleep($waitTime); // wait specified time until we restart the transaction
  683. } else {
  684. // if the transaction still fail after $maxRetries retries, we throw out the exception
  685. Logger::error('Finally giving up restarting the same transaction again and again, last message: ' . $e->getMessage());
  686. throw $e;
  687. }
  688. } else {
  689. throw $e;
  690. }
  691. }
  692. }
  693. $additionalTags = [];
  694. if (isset($updatedChildren) && is_array($updatedChildren)) {
  695. foreach ($updatedChildren as $objectId) {
  696. $tag = 'object_' . $objectId;
  697. $additionalTags[] = $tag;
  698. // remove the child also from registry (internal cache) to avoid path inconsistencies during long running scripts, such as CLI
  699. RuntimeCache::set($tag, null);
  700. }
  701. }
  702. $this->clearDependentCache($additionalTags);
  703. $postEvent = new DataObjectEvent($this, $params);
  704. if ($isUpdate) {
  705. if ($differentOldPath) {
  706. $postEvent->setArgument('oldPath', $differentOldPath);
  707. }
  708. $this->dispatchEvent($postEvent, DataObjectEvents::POST_UPDATE);
  709. } else {
  710. self::setDisableDirtyDetection($isDirtyDetectionDisabled);
  711. $this->dispatchEvent($postEvent, DataObjectEvents::POST_ADD);
  712. }
  713. return $this;
  714. } catch (\Exception $e) {
  715. $failureEvent = new DataObjectEvent($this, $params);
  716. $failureEvent->setArgument('exception', $e);
  717. if ($isUpdate) {
  718. $this->dispatchEvent($failureEvent, DataObjectEvents::POST_UPDATE_FAILURE);
  719. } else {
  720. $this->dispatchEvent($failureEvent, DataObjectEvents::POST_ADD_FAILURE);
  721. }
  722. throw $e;
  723. }
  724. }
  725. /**
  726. * @internal
  727. *
  728. * @throws \Exception|DuplicateFullPathException
  729. */
  730. protected function correctPath()
  731. {
  732. // set path
  733. if ($this->getId() != 1) { // not for the root node
  734. if (!Element\Service::isValidKey($this->getKey(), 'object')) {
  735. throw new \Exception('invalid key for object with id [ '.$this->getId().' ] key is: [' . $this->getKey() . ']');
  736. }
  737. if ($this->getParentId() == $this->getId()) {
  738. throw new \Exception("ParentID and ID is identical, an element can't be the parent of itself.");
  739. }
  740. $parent = DataObject::getById($this->getParentId());
  741. if ($parent) {
  742. // use the parent's path from the database here (getCurrentFullPath), to ensure the path really exists and does not rely on the path
  743. // that is currently in the parent object (in memory), because this might have changed but wasn't not saved
  744. $this->setPath(str_replace('//', '/', $parent->getCurrentFullPath().'/'));
  745. } else {
  746. trigger_deprecation(
  747. 'pimcore/pimcore',
  748. '10.5',
  749. 'Fallback for parentId will be removed in Pimcore 11.',
  750. );
  751. // parent document doesn't exist anymore, set the parent to to root
  752. $this->setParentId(1);
  753. $this->setPath('/');
  754. }
  755. if (strlen($this->getKey()) < 1) {
  756. throw new \Exception('DataObject requires key');
  757. }
  758. } elseif ($this->getId() == 1) {
  759. // some data in root node should always be the same
  760. $this->setParentId(0);
  761. $this->setPath('/');
  762. $this->setKey('');
  763. $this->setType(DataObject::OBJECT_TYPE_FOLDER);
  764. }
  765. if (Service::pathExists($this->getRealFullPath())) {
  766. $duplicate = DataObject::getByPath($this->getRealFullPath());
  767. if ($duplicate instanceof self && $duplicate->getId() != $this->getId()) {
  768. $duplicateFullPathException = new DuplicateFullPathException('Duplicate full path [ '.$this->getRealFullPath().' ] - cannot save object');
  769. $duplicateFullPathException->setDuplicateElement($duplicate);
  770. throw $duplicateFullPathException;
  771. }
  772. }
  773. $this->validatePathLength();
  774. }
  775. /**
  776. * @internal
  777. *
  778. * @param bool|null $isUpdate
  779. * @param array $params
  780. *
  781. * @throws \Exception
  782. */
  783. protected function update($isUpdate = null, $params = [])
  784. {
  785. $this->updateModificationInfos();
  786. // save properties
  787. $this->getProperties();
  788. $this->getDao()->deleteAllProperties();
  789. if (is_array($this->getProperties()) && count($this->getProperties()) > 0) {
  790. foreach ($this->getProperties() as $property) {
  791. if (!$property->getInherited()) {
  792. $property->setDao(null);
  793. $property->setCid($this->getId());
  794. $property->setCtype('object');
  795. $property->setCpath($this->getRealFullPath());
  796. $property->save();
  797. }
  798. }
  799. }
  800. // save dependencies
  801. $d = new Model\Dependency();
  802. $d->setSourceType('object');
  803. $d->setSourceId($this->getId());
  804. foreach ($this->resolveDependencies() as $requirement) {
  805. if ($requirement['id'] == $this->getId() && $requirement['type'] === 'object') {
  806. // dont't add a reference to yourself
  807. continue;
  808. }
  809. $d->addRequirement($requirement['id'], $requirement['type']);
  810. }
  811. $d->save();
  812. //set object to registry
  813. RuntimeCache::set(self::getCacheKey($this->getId()), $this);
  814. }
  815. /**
  816. * {@inheritdoc}
  817. */
  818. public function clearDependentCache($additionalTags = [])
  819. {
  820. self::clearDependentCacheByObjectId($this->getId(), $additionalTags);
  821. }
  822. /**
  823. * @internal
  824. *
  825. * @param int $objectId
  826. * @param array $additionalTags
  827. */
  828. public static function clearDependentCacheByObjectId($objectId, $additionalTags = [])
  829. {
  830. if (!$objectId) {
  831. throw new \Exception('object ID missing');
  832. }
  833. try {
  834. $tags = ['object_' . $objectId, 'object_properties', 'output'];
  835. $tags = array_merge($tags, $additionalTags);
  836. Cache::clearTags($tags);
  837. } catch (\Exception $e) {
  838. Logger::crit((string) $e);
  839. }
  840. }
  841. /**
  842. * @internal
  843. *
  844. * @param int $index
  845. */
  846. public function saveIndex($index)
  847. {
  848. $this->getDao()->saveIndex($index);
  849. $this->clearDependentCache();
  850. }
  851. /**
  852. * @return string
  853. */
  854. public function getFullPath()
  855. {
  856. $path = $this->getPath() . $this->getKey();
  857. return $path;
  858. }
  859. /**
  860. * @return string
  861. */
  862. public function getRealPath()
  863. {
  864. return $this->getPath();
  865. }
  866. /**
  867. * @return string
  868. */
  869. public function getRealFullPath()
  870. {
  871. return $this->getFullPath();
  872. }
  873. /**
  874. * @return int|null
  875. */
  876. public function getParentId()
  877. {
  878. $parentId = parent::getParentId();
  879. // fall back to parent if no ID is set but we have a parent object
  880. if (!$parentId && $this->parent) {
  881. return $this->parent->getId();
  882. }
  883. return $parentId;
  884. }
  885. /**
  886. * @return string
  887. */
  888. public function getType()
  889. {
  890. return $this->o_type;
  891. }
  892. /**
  893. * @return string|null
  894. */
  895. public function getKey()
  896. {
  897. return $this->o_key;
  898. }
  899. /**
  900. * @return int
  901. */
  902. public function getIndex()
  903. {
  904. return $this->o_index;
  905. }
  906. /**
  907. * @param int $parentId
  908. *
  909. * @return $this
  910. */
  911. public function setParentId($parentId)
  912. {
  913. $parentId = (int) $parentId;
  914. if ($parentId != $this->parentId) {
  915. $this->markFieldDirty('parentId');
  916. }
  917. parent::setParentId($parentId);
  918. $this->o_siblings = [];
  919. $this->o_hasSiblings = [];
  920. return $this;
  921. }
  922. /**
  923. * @param string $o_type
  924. *
  925. * @return $this
  926. */
  927. public function setType($o_type)
  928. {
  929. $this->o_type = $o_type;
  930. return $this;
  931. }
  932. /**
  933. * @param string $o_key
  934. *
  935. * @return $this
  936. */
  937. public function setKey($o_key)
  938. {
  939. $this->o_key = (string)$o_key;
  940. return $this;
  941. }
  942. /**
  943. * @param int $o_index
  944. *
  945. * @return $this
  946. */
  947. public function setIndex($o_index)
  948. {
  949. $this->o_index = (int) $o_index;
  950. return $this;
  951. }
  952. /**
  953. * @param string|null $childrenSortBy
  954. */
  955. public function setChildrenSortBy($childrenSortBy)
  956. {
  957. if ($this->o_childrenSortBy !== $childrenSortBy) {
  958. $this->o_children = [];
  959. $this->o_hasChildren = [];
  960. }
  961. $this->o_childrenSortBy = $childrenSortBy;
  962. }
  963. /**
  964. * @param DataObject[]|null $children
  965. * @param array $objectTypes
  966. * @param bool $includingUnpublished
  967. *
  968. * @return $this
  969. */
  970. public function setChildren($children, array $objectTypes = [self::OBJECT_TYPE_OBJECT, self::OBJECT_TYPE_FOLDER], $includingUnpublished = false)
  971. {
  972. if ($children === null) {
  973. // unset all cached children
  974. $this->o_children = [];
  975. $this->o_hasChildren = [];
  976. } elseif (is_array($children)) {
  977. //default cache key
  978. $cacheKey = $this->getListingCacheKey([$objectTypes, $includingUnpublished]);
  979. $this->o_children[$cacheKey] = $children;
  980. $this->o_hasChildren[$cacheKey] = (bool) count($children);
  981. }
  982. return $this;
  983. }
  984. /**
  985. * @return self|null
  986. */
  987. public function getParent() /** : ?self **/
  988. {
  989. $parent = parent::getParent();
  990. return $parent instanceof AbstractObject ? $parent : null;
  991. }
  992. /**
  993. * @param self|null $parent
  994. *
  995. * @return $this
  996. */
  997. public function setParent($parent)
  998. {
  999. $newParentId = $parent instanceof self ? $parent->getId() : 0;
  1000. $this->setParentId($newParentId);
  1001. $this->parent = $parent;
  1002. return $this;
  1003. }
  1004. /**
  1005. * @return string
  1006. */
  1007. public function getChildrenSortBy()
  1008. {
  1009. return $this->o_childrenSortBy ?? self::OBJECT_CHILDREN_SORT_BY_DEFAULT;
  1010. }
  1011. /**
  1012. * @param string $method
  1013. * @param array $args
  1014. *
  1015. * @return mixed
  1016. *
  1017. * @throws \Exception
  1018. */
  1019. public function __call($method, $args)
  1020. {
  1021. // compatibility mode (they do not have any set_oXyz() methods anymore)
  1022. if (preg_match('/^(get|set)o_/i', $method)) {
  1023. $newMethod = preg_replace('/^(get|set)o_/i', '$1', $method);
  1024. trigger_deprecation(
  1025. 'pimcore/pimcore',
  1026. '10.6',
  1027. sprintf('using "%s" is deprecated, please use "%s" instead. using "%s" wont work in Pimcore 11.', $method, $newMethod, $method)
  1028. );
  1029. if (method_exists($this, $newMethod)) {
  1030. $r = call_user_func_array([$this, $newMethod], $args);
  1031. return $r;
  1032. }
  1033. }
  1034. return parent::__call($method, $args);
  1035. }
  1036. /**
  1037. * @return bool
  1038. */
  1039. public static function doNotRestoreKeyAndPath()
  1040. {
  1041. return self::$doNotRestoreKeyAndPath;
  1042. }
  1043. /**
  1044. * @param bool $doNotRestoreKeyAndPath
  1045. */
  1046. public static function setDoNotRestoreKeyAndPath($doNotRestoreKeyAndPath)
  1047. {
  1048. self::$doNotRestoreKeyAndPath = $doNotRestoreKeyAndPath;
  1049. }
  1050. /**
  1051. * @param string $fieldName
  1052. * @param string|null $language
  1053. *
  1054. * @throws \Exception
  1055. *
  1056. * @return mixed
  1057. */
  1058. public function get($fieldName, $language = null)
  1059. {
  1060. if (!$fieldName) {
  1061. throw new \Exception('Field name must not be empty.');
  1062. }
  1063. return $this->{'get'.ucfirst($fieldName)}($language);
  1064. }
  1065. /**
  1066. * @param string $fieldName
  1067. * @param mixed $value
  1068. * @param string|null $language
  1069. *
  1070. * @throws \Exception
  1071. *
  1072. * @return mixed
  1073. */
  1074. public function set($fieldName, $value, $language = null)
  1075. {
  1076. if (!$fieldName) {
  1077. throw new \Exception('Field name must not be empty.');
  1078. }
  1079. return $this->{'set'.ucfirst($fieldName)}($value, $language);
  1080. }
  1081. /**
  1082. * @internal
  1083. *
  1084. * @return bool
  1085. */
  1086. public static function isDirtyDetectionDisabled()
  1087. {
  1088. return self::$disableDirtyDetection;
  1089. }
  1090. /**
  1091. * @internal
  1092. *
  1093. * @param bool $disableDirtyDetection
  1094. */
  1095. public static function setDisableDirtyDetection(bool $disableDirtyDetection)
  1096. {
  1097. self::$disableDirtyDetection = $disableDirtyDetection;
  1098. }
  1099. /**
  1100. * @internal
  1101. */
  1102. public static function disableDirtyDetection()
  1103. {
  1104. self::setDisableDirtyDetection(true);
  1105. }
  1106. /**
  1107. * @internal
  1108. */
  1109. public static function enableDirtyDetection()
  1110. {
  1111. self::setDisableDirtyDetection(false);
  1112. }
  1113. /**
  1114. * @internal
  1115. *
  1116. * @param array $args
  1117. *
  1118. * @return string
  1119. */
  1120. protected function getListingCacheKey(array $args = [])
  1121. {
  1122. $objectTypes = $args[0] ?? [self::OBJECT_TYPE_OBJECT, self::OBJECT_TYPE_FOLDER];
  1123. $includingUnpublished = (bool)($args[1] ?? false);
  1124. if (is_array($objectTypes)) {
  1125. $objectTypes = implode('_', $objectTypes);
  1126. }
  1127. $cacheKey = $objectTypes . (!empty($includingUnpublished) ? '_' : '') . (string)$includingUnpublished;
  1128. return $cacheKey;
  1129. }
  1130. /**
  1131. * @param string | null $o_reverseSort
  1132. *
  1133. * @return AbstractObject
  1134. */
  1135. public function setChildrenSortOrder(?string $o_reverseSort): Element\ElementInterface
  1136. {
  1137. $this->o_childrenSortOrder = $o_reverseSort;
  1138. return $this;
  1139. }
  1140. /**
  1141. * @return string
  1142. */
  1143. public function getChildrenSortOrder(): string
  1144. {
  1145. return $this->o_childrenSortOrder ?? self::OBJECT_CHILDREN_SORT_ORDER_DEFAULT;
  1146. }
  1147. /**
  1148. * load lazy loaded fields before cloning
  1149. */
  1150. public function __clone()
  1151. {
  1152. parent::__clone();
  1153. // renew references when cloning
  1154. foreach (['id', 'path', 'creationDate', 'userOwner', 'versionCount', 'modificationDate', 'locked', 'parent', 'properties', 'userModification', 'parentId'] as $referenceField) {
  1155. $oldValue = $this->$referenceField;
  1156. unset($this->$referenceField);
  1157. $this->$referenceField = $oldValue;
  1158. $this->{'o_'.$referenceField} = &$this->$referenceField;
  1159. }
  1160. $this->o_parent = null;
  1161. // note that o_children is currently needed for the recycle bin
  1162. $this->o_hasSiblings = [];
  1163. $this->o_siblings = [];
  1164. }
  1165. /**
  1166. * @param string $method
  1167. * @param array $arguments
  1168. *
  1169. * @return mixed
  1170. *
  1171. * @throws \Exception
  1172. */
  1173. public static function __callStatic($method, $arguments)
  1174. {
  1175. $propertyName = lcfirst(preg_replace('/^getBy/i', '', $method));
  1176. $realPropertyName = 'o_'.$propertyName;
  1177. $db = \Pimcore\Db::get();
  1178. if (in_array(strtolower($realPropertyName), self::$objectColumns)) {
  1179. $arguments = array_pad($arguments, 4, 0);
  1180. [$value, $limit, $offset, $objectTypes] = $arguments;
  1181. $defaultCondition = $realPropertyName.' = '.Db::get()->quote($value).' ';
  1182. $listConfig = [
  1183. 'condition' => $defaultCondition,
  1184. ];
  1185. if (!is_array($limit)) {
  1186. if ($limit) {
  1187. $listConfig['limit'] = $limit;
  1188. }
  1189. if ($offset) {
  1190. $listConfig['offset'] = $offset;
  1191. }
  1192. } else {
  1193. $listConfig = array_merge($listConfig, $limit);
  1194. $limitCondition = $limit['condition'] ?? '';
  1195. $listConfig['condition'] = $defaultCondition.$limitCondition;
  1196. }
  1197. $list = static::makeList($listConfig, $objectTypes);
  1198. if (isset($listConfig['limit']) && $listConfig['limit'] == 1) {
  1199. $elements = $list->getObjects();
  1200. return isset($elements[0]) ? $elements[0] : null;
  1201. }
  1202. return $list;
  1203. }
  1204. // there is no property for the called method, so throw an exception
  1205. Logger::error('Class: DataObject\\AbstractObject => call to undefined static method ' . $method);
  1206. throw new \Exception('Call to undefined static method ' . $method . ' in class DataObject\\AbstractObject');
  1207. }
  1208. /**
  1209. * @param array $listConfig
  1210. * @param mixed $objectTypes
  1211. *
  1212. * @return Listing
  1213. *
  1214. * @throws \Exception
  1215. */
  1216. protected static function makeList(array $listConfig, mixed $objectTypes): Listing
  1217. {
  1218. $list = static::getList($listConfig);
  1219. // Check if variants, in addition to objects, to be fetched
  1220. if (!empty($objectTypes)) {
  1221. if (\array_diff($objectTypes, [static::OBJECT_TYPE_VARIANT, static::OBJECT_TYPE_OBJECT])) {
  1222. Logger::error('Class: DataObject\\AbstractObject => Unsupported object type in array ' . implode(',', $objectTypes));
  1223. throw new \Exception('Unsupported object type in array [' . implode(',', $objectTypes) . '] in class DataObject\\AbstractObject');
  1224. }
  1225. $list->setObjectTypes($objectTypes);
  1226. }
  1227. return $list;
  1228. }
  1229. public function __wakeup()
  1230. {
  1231. $propertyMappings = [
  1232. 'o_id' => 'id',
  1233. 'o_path' => 'path',
  1234. 'o_creationDate' => 'creationDate',
  1235. 'o_userOwner' => 'userOwner',
  1236. 'o_versionCount' => 'versionCount',
  1237. 'o_locked' => 'locked',
  1238. 'o_parent' => 'parent',
  1239. 'o_properties' => 'properties',
  1240. 'o_userModification' => 'userModification',
  1241. 'o_modificationDate' => 'modificationDate',
  1242. 'o_parentId' => 'parentId',
  1243. ];
  1244. foreach ($propertyMappings as $oldProperty => $newProperty) {
  1245. if ($this->$newProperty === null) {
  1246. $this->$newProperty = $this->$oldProperty;
  1247. $this->$oldProperty = & $this->$newProperty;
  1248. }
  1249. }
  1250. parent::__wakeup();
  1251. }
  1252. }