vendor/jackalope/jackalope/src/Jackalope/Node.php line 1796

Open in your IDE?
  1. <?php
  2. namespace Jackalope;
  3. use ArrayIterator;
  4. use Iterator;
  5. use IteratorAggregate;
  6. use Exception;
  7. use InvalidArgumentException;
  8. use Jackalope\NodeType\NodeType;
  9. use LogicException;
  10. use PHPCR\AccessDeniedException;
  11. use PHPCR\Lock\LockException;
  12. use PHPCR\NamespaceException;
  13. use PHPCR\NodeType\NodeDefinitionInterface;
  14. use PHPCR\NodeType\NodeTypeInterface;
  15. use PHPCR\PropertyType;
  16. use PHPCR\NodeInterface;
  17. use PHPCR\NodeType\ConstraintViolationException;
  18. use PHPCR\NodeType\NoSuchNodeTypeException;
  19. use PHPCR\RepositoryException;
  20. use PHPCR\PathNotFoundException;
  21. use PHPCR\ItemNotFoundException;
  22. use PHPCR\InvalidItemStateException;
  23. use PHPCR\ItemExistsException;
  24. use PHPCR\UnsupportedRepositoryOperationException;
  25. use PHPCR\Util\PathHelper;
  26. use PHPCR\Util\NodeHelper;
  27. use PHPCR\Util\UUIDHelper;
  28. use PHPCR\ValueFormatException;
  29. use PHPCR\Version\VersionException;
  30. /**
  31. * {@inheritDoc}
  32. *
  33. * @license http://www.apache.org/licenses Apache License Version 2.0, January 2004
  34. * @license http://opensource.org/licenses/MIT MIT License
  35. *
  36. * @api
  37. */
  38. class Node extends Item implements IteratorAggregate, NodeInterface
  39. {
  40. /**
  41. * The index if this is a same-name sibling.
  42. *
  43. * TODO: fully implement same-name siblings
  44. * @var int
  45. */
  46. protected $index = 1;
  47. /**
  48. * The primary type name of this node
  49. *
  50. * @var string
  51. */
  52. protected $primaryType;
  53. /**
  54. * mapping of property name to PropertyInterface objects.
  55. *
  56. * all properties are instantiated in the constructor
  57. *
  58. * OPTIMIZE: lazy instantiate property objects, just have local array of values
  59. *
  60. * @var Property[]
  61. */
  62. protected $properties = [];
  63. /**
  64. * keep track of properties to be deleted until the save operation was successful.
  65. *
  66. * this is needed in order to track deletions in case of refresh
  67. *
  68. * keys are the property names, values the properties (in state deleted)
  69. */
  70. protected $deletedProperties = [];
  71. /**
  72. * ordered list of the child node names
  73. *
  74. * @var array
  75. */
  76. protected $nodes = [];
  77. /**
  78. * ordered list of the child node names as known to be at the backend
  79. *
  80. * used to calculate reordering operations if orderBefore() was used
  81. *
  82. * @var array
  83. */
  84. protected $originalNodesOrder = null;
  85. /**
  86. * Cached instance of the node definition that defines this node
  87. *
  88. * @var NodeDefinitionInterface
  89. * @see Node::getDefinition()
  90. */
  91. protected $definition;
  92. /**
  93. * Create a new node instance with data from the storage layer
  94. *
  95. * This is only to be called by the Factory::get() method even inside the
  96. * Jackalope implementation to allow for custom implementations of Nodes.
  97. *
  98. * @param FactoryInterface $factory the object factory
  99. * @param array $rawData in the format as returned from TransportInterface::getNode
  100. * @param string $path the absolute path of this node
  101. * @param Session $session
  102. * @param ObjectManager $objectManager
  103. * @param boolean $new set to true if this is a new node being created.
  104. * Defaults to false which means the node is loaded from storage.
  105. *
  106. * @see TransportInterface::getNode()
  107. *
  108. * @throws RepositoryException
  109. *
  110. * @private
  111. */
  112. public function __construct(FactoryInterface $factory, $rawData, $path, Session $session, ObjectManager $objectManager, $new = false)
  113. {
  114. parent::__construct($factory, $path, $session, $objectManager, $new);
  115. $this->isNode = true;
  116. $this->parseData($rawData, false);
  117. }
  118. /**
  119. * Initialize or update this object with raw data from backend.
  120. *
  121. * @param array $rawData in the format as returned from Jackalope\Transport\TransportInterface
  122. * @param boolean $update whether to initialize this object or update
  123. * @param boolean $keepChanges only used if $update is true, same as $keepChanges in refresh()
  124. *
  125. * @see Node::__construct()
  126. * @see Node::refresh()
  127. *
  128. * @throws \InvalidArgumentException
  129. * @throws LockException
  130. * @throws ConstraintViolationException
  131. * @throws RepositoryException
  132. * @throws ValueFormatException
  133. * @throws VersionException
  134. */
  135. private function parseData($rawData, $update, $keepChanges = false)
  136. {
  137. //TODO: refactor to use hash array instead of stdClass struct
  138. if ($update) {
  139. // keep backup of old state so we can remove what needs to be removed
  140. $oldNodes = array_flip(array_values($this->nodes));
  141. $oldProperties = $this->properties;
  142. }
  143. /*
  144. * we collect all nodes coming from the backend. if we update with
  145. * $keepChanges, we use this to update the node list rather than losing
  146. * reorders
  147. *
  148. * properties are easy as they are not ordered.
  149. */
  150. $nodesInBackend = [];
  151. foreach ($rawData as $key => $value) {
  152. $node = false; // reset to avoid trouble
  153. if (is_object($value)) {
  154. // this is a node. add it if
  155. if (! $update // init new node
  156. || ! $keepChanges // want to discard changes
  157. || isset($oldNodes[$key]) // it was already existing before reloading
  158. || ! ($node = $this->objectManager->getCachedNode($this->path . '/' . $key)) // we know nothing about it
  159. ) {
  160. // for all those cases, if the node was moved away or is deleted in current session, we do not add it
  161. if (! $this->objectManager->isNodeMoved($this->path . '/' . $key)
  162. && ! $this->objectManager->isNodeDeleted($this->path . '/' . $key)
  163. ) {
  164. // otherwise we (re)load a node from backend but a child has been moved away already
  165. $nodesInBackend[] = $key;
  166. }
  167. }
  168. if ($update) {
  169. unset($oldNodes[$key]);
  170. }
  171. } else {
  172. //property or meta information
  173. /* Property type declarations start with :, the value then is
  174. * the type string from the NodeType constants. We skip that and
  175. * look at the type when we encounter the value of the property.
  176. *
  177. * If its a binary data, we only get the type declaration and
  178. * no data. Then the $value of the type declaration is not the
  179. * type string for binary, but the number of bytes of the
  180. * property - resp. array of number of bytes.
  181. *
  182. * The magic property ::NodeIteratorSize tells this node has no
  183. * children. Ignore that info for now. We might optimize with
  184. * this info once we do prefetch nodes.
  185. */
  186. if (0 === strpos($key, ':')) {
  187. if ((is_int($value) || is_array($value))
  188. && $key != '::NodeIteratorSize'
  189. ) {
  190. // This is a binary property and we just got its length with no data
  191. $key = substr($key, 1);
  192. if (!isset($rawData->$key)) {
  193. $binaries[$key] = $value;
  194. if ($update) {
  195. unset($oldProperties[$key]);
  196. }
  197. if (isset($this->properties[$key])) {
  198. // refresh existing binary, this will only happen in update
  199. // only update length
  200. if (! ($keepChanges && $this->properties[$key]->isModified())) {
  201. $this->properties[$key]->_setLength($value);
  202. if ($this->properties[$key]->isDirty()) {
  203. $this->properties[$key]->setClean();
  204. }
  205. }
  206. } else {
  207. // this will always fall into the creation mode
  208. $this->_setProperty($key, $value, PropertyType::BINARY, true);
  209. }
  210. }
  211. } //else this is a type declaration
  212. //skip this entry (if its binary, its already processed
  213. continue;
  214. }
  215. if ($update && array_key_exists($key, $this->properties)) {
  216. unset($oldProperties[$key]);
  217. $prop = $this->properties[$key];
  218. if ($keepChanges && $prop->isModified()) {
  219. continue;
  220. }
  221. } elseif ($update && array_key_exists($key, $this->deletedProperties)) {
  222. if ($keepChanges) {
  223. // keep the delete
  224. continue;
  225. } else {
  226. // restore the property
  227. $this->properties[$key] = $this->deletedProperties[$key];
  228. $this->properties[$key]->setClean();
  229. // now let the loop update the value. no need to talk to ObjectManager as it
  230. // does not store property deletions
  231. }
  232. }
  233. switch ($key) {
  234. case 'jcr:index':
  235. $this->index = $value;
  236. break;
  237. case 'jcr:primaryType':
  238. $this->primaryType = $value;
  239. // type information is exposed as property too,
  240. // although there exist more specific methods
  241. $this->_setProperty('jcr:primaryType', $value, PropertyType::NAME, true);
  242. break;
  243. case 'jcr:mixinTypes':
  244. // type information is exposed as property too,
  245. // although there exist more specific methods
  246. $this->_setProperty($key, $value, PropertyType::NAME, true);
  247. break;
  248. // OPTIMIZE: do not instantiate properties until needed
  249. default:
  250. if (isset($rawData->{':' . $key})) {
  251. /*
  252. * this is an inconsistency between jackrabbit and
  253. * dbal transport: jackrabbit has type name, dbal
  254. * delivers numeric type.
  255. * we should eventually fix the format returned by
  256. * transport and either have jackrabbit transport
  257. * do the conversion or let dbal store a string
  258. * value instead of numerical.
  259. */
  260. $type = is_numeric($rawData->{':' . $key})
  261. ? $rawData->{':' . $key}
  262. : PropertyType::valueFromName($rawData->{':' . $key});
  263. } else {
  264. $type = $this->valueConverter->determineType($value);
  265. }
  266. $this->_setProperty($key, $value, $type, true);
  267. break;
  268. }
  269. }
  270. }
  271. if ($update) {
  272. if ($keepChanges) {
  273. // we keep changes. merge new nodes to the right place
  274. $previous = null;
  275. $newFromBackend = array_diff($nodesInBackend, array_intersect($this->nodes, $nodesInBackend));
  276. foreach ($newFromBackend as $name) {
  277. $pos = array_search($name, $nodesInBackend);
  278. if (is_array($this->originalNodesOrder)) {
  279. // update original order to send the correct reorderings
  280. array_splice($this->originalNodesOrder, $pos, 0, $name);
  281. }
  282. if ($pos === 0) {
  283. array_unshift($this->nodes, $name);
  284. } else {
  285. // do we find the predecessor of the new node in the list?
  286. $insert = array_search($nodesInBackend[$pos-1], $this->nodes);
  287. if (false !== $insert) {
  288. array_splice($this->nodes, $insert + 1, 0, $name);
  289. } else {
  290. // failed to find predecessor, add to the end
  291. $this->nodes[] = $name;
  292. }
  293. }
  294. }
  295. } else {
  296. // discard changes, just overwrite node list
  297. $this->nodes = $nodesInBackend;
  298. $this->originalNodesOrder = null;
  299. }
  300. foreach ($oldProperties as $name => $property) {
  301. if (! ($keepChanges && ($property->isNew()))) {
  302. // may not call remove(), we don't want another delete with
  303. // the backend to be attempted
  304. $this->properties[$name]->setDeleted();
  305. unset($this->properties[$name]);
  306. }
  307. }
  308. // notify nodes that where not received again that they disappeared
  309. foreach ($oldNodes as $name => $index) {
  310. if ($this->objectManager->purgeDisappearedNode($this->path . '/' . $name, $keepChanges)) {
  311. // drop, it was not a new child
  312. if ($keepChanges) { // otherwise we overwrote $this->nodes with the backend
  313. $id = array_search($name, $this->nodes);
  314. if (false !== $id) {
  315. unset($this->nodes[$id]);
  316. }
  317. }
  318. }
  319. }
  320. } else {
  321. // new node loaded from backend
  322. $this->nodes = $nodesInBackend;
  323. }
  324. }
  325. /**
  326. * Creates a new node at the specified $relPath
  327. *
  328. * {@inheritDoc}
  329. *
  330. * In Jackalope, the child node type definition is immediately applied if no
  331. * primaryNodeTypeName is specified.
  332. *
  333. * The PathNotFoundException and ConstraintViolationException are thrown
  334. * immediately.
  335. * Version and Lock related exceptions are delayed until save.
  336. *
  337. * @return NodeInterface the node that was added
  338. *
  339. * @api
  340. */
  341. public function addNode($relPath, $primaryNodeTypeName = null)
  342. {
  343. $relPath = (string)$relPath;
  344. $this->checkState();
  345. $ntm = $this->session->getWorkspace()->getNodeTypeManager();
  346. // are we not the immediate parent?
  347. if (strpos($relPath, '/') !== false) {
  348. // forward to real parent
  349. $relPath = PathHelper::absolutizePath($relPath, $this->getPath(), true);
  350. $parentPath = PathHelper::getParentPath($relPath);
  351. $newName = PathHelper::getNodeName($relPath);
  352. try {
  353. $parentNode = $this->objectManager->getNodeByPath($parentPath);
  354. } catch (ItemNotFoundException $e) {
  355. //we have to throw a different exception if there is a property
  356. // with that name than if there is nothing at the path at all.
  357. // lets see if the property exists
  358. if ($this->session->propertyExists($parentPath)) {
  359. throw new ConstraintViolationException("Node '{$this->path}': Not allowed to add a node below property at $parentPath");
  360. }
  361. throw new PathNotFoundException($e->getMessage(), $e->getCode(), $e);
  362. }
  363. return $parentNode->addNode($newName, $primaryNodeTypeName);
  364. }
  365. if (null === $primaryNodeTypeName) {
  366. if ($this->primaryType === 'rep:root') {
  367. $primaryNodeTypeName = 'nt:unstructured';
  368. } else {
  369. $type = $ntm->getNodeType($this->primaryType);
  370. $nodeDefinitions = $type->getChildNodeDefinitions();
  371. foreach ($nodeDefinitions as $def) {
  372. if (!is_null($def->getDefaultPrimaryType())) {
  373. $primaryNodeTypeName = $def->getDefaultPrimaryTypeName();
  374. break;
  375. }
  376. }
  377. }
  378. if (is_null($primaryNodeTypeName)) {
  379. throw new ConstraintViolationException("No matching child node definition found for `$relPath' in type `{$this->primaryType}' for node '{$this->path}'. Please specify the type explicitly.");
  380. }
  381. }
  382. // create child node
  383. //sanity check: no index allowed. TODO: we should verify this is a valid node name
  384. if (false !== strpos($relPath, ']')) {
  385. throw new RepositoryException("The node '{$this->path}' does not allow an index in name of newly created node: $relPath");
  386. }
  387. if (in_array($relPath, $this->nodes, true)) {
  388. throw new ItemExistsException("The node '{$this->path}' already has a child named '$relPath''."); //TODO: same-name siblings if nodetype allows for them
  389. }
  390. $data = ['jcr:primaryType' => $primaryNodeTypeName];
  391. $path = $this->getChildPath($relPath);
  392. $node = $this->factory->get(Node::class, [$data, $path, $this->session, $this->objectManager, true]);
  393. $this->addChildNode($node, false); // no need to check the state, we just checked when entering this method
  394. $this->objectManager->addNode($path, $node);
  395. if (is_array($this->originalNodesOrder)) {
  396. // new nodes are added at the end
  397. $this->originalNodesOrder[] = $relPath;
  398. }
  399. //by definition, adding a node sets the parent to modified
  400. $this->setModified();
  401. return $node;
  402. }
  403. /**
  404. * {@inheritDoc}
  405. *
  406. * @return NodeInterface the newly created node
  407. *
  408. * @throws InvalidArgumentException
  409. * @throws ItemExistsException
  410. * @throws PathNotFoundException
  411. * @throws RepositoryException
  412. * @api
  413. */
  414. public function addNodeAutoNamed($nameHint = null, $primaryNodeTypeName = null)
  415. {
  416. $name = NodeHelper::generateAutoNodeName(
  417. $this->nodes,
  418. $this->session->getWorkspace()->getNamespaceRegistry()->getNamespaces(),
  419. 'jcr',
  420. $nameHint
  421. );
  422. return $this->addNode($name, $primaryNodeTypeName);
  423. }
  424. /**
  425. * Jackalope implements this feature and updates the position of the
  426. * existing child at srcChildRelPath to be in the list immediately before
  427. * destChildRelPath.
  428. *
  429. * {@inheritDoc}
  430. *
  431. * Jackalope has no implementation-specific ordering restriction so no
  432. * \PHPCR\ConstraintViolationException is expected. VersionException and
  433. * LockException are not tested immediately but thrown on save.
  434. *
  435. * @api
  436. */
  437. public function orderBefore($srcChildRelPath, $destChildRelPath)
  438. {
  439. if ($srcChildRelPath === $destChildRelPath) {
  440. //nothing to move
  441. return;
  442. }
  443. if (null === $this->originalNodesOrder) {
  444. $this->originalNodesOrder = $this->nodes;
  445. }
  446. $this->nodes = NodeHelper::orderBeforeArray($srcChildRelPath, $destChildRelPath, $this->nodes);
  447. $this->setModified();
  448. }
  449. /**
  450. * {@inheritDoc}
  451. *
  452. * @throws PathNotFoundException
  453. *
  454. * @api
  455. * @throws AccessDeniedException
  456. * @throws ItemNotFoundException
  457. * @throws \InvalidArgumentException
  458. */
  459. public function rename($newName)
  460. {
  461. $names = (array) $this->getParent()->getNodeNames();
  462. $pos = array_search($this->name, $names);
  463. $next = isset($names[$pos + 1]) ? $names[$pos + 1] : null;
  464. $newPath = $this->parentPath . '/' . $newName;
  465. if (substr($newPath, 0, 2) === '//') {
  466. $newPath = substr($newPath, 1);
  467. }
  468. $this->session->move($this->path, $newPath);
  469. if ($next) {
  470. $this->getParent()->orderBefore($newName, $next);
  471. }
  472. }
  473. /**
  474. * Determine whether the children of this node need to be reordered
  475. *
  476. * @return boolean
  477. *
  478. * @private
  479. */
  480. public function needsChildReordering()
  481. {
  482. return (bool) $this->originalNodesOrder;
  483. }
  484. /**
  485. * Returns the orderBefore commands to be applied to the childnodes
  486. * to get from the original order to the new one
  487. *
  488. * @return array of arrays with 2 fields: name of node to order before second name
  489. *
  490. * @throws AccessDeniedException
  491. * @throws ItemNotFoundException
  492. *
  493. * @private
  494. */
  495. public function getOrderCommands()
  496. {
  497. if (! $this->originalNodesOrder) {
  498. return [];
  499. }
  500. $reorders = NodeHelper::calculateOrderBefore($this->originalNodesOrder, $this->nodes);
  501. $this->originalNodesOrder = null;
  502. return $reorders;
  503. }
  504. /**
  505. * {@inheritDoc}
  506. *
  507. * @param boolean $validate When false, node types are not asked to validate
  508. * whether operation is allowed
  509. *
  510. * @return \PHPCR\PropertyInterface The new resp. updated Property object
  511. *
  512. * @throws InvalidItemStateException
  513. * @throws NamespaceException
  514. * @throws \InvalidArgumentException
  515. * @throws AccessDeniedException
  516. * @throws ItemNotFoundException
  517. *
  518. * @api
  519. */
  520. public function setProperty($name, $value, $type = PropertyType::UNDEFINED, $validate = true)
  521. {
  522. $this->checkState();
  523. // abort early when the node value is not changed
  524. // for multivalue, === is only true when array keys and values match. this is exactly what we need.
  525. try {
  526. if (array_key_exists($name, $this->properties) && $this->properties[$name]->getValue() === $value) {
  527. return $this->properties[$name];
  528. }
  529. } catch (RepositoryException $e) {
  530. // if anything goes wrong trying to get the property value, move on and don't return early
  531. }
  532. if ($validate && 'jcr:uuid' === $name && !$this->isNodeType('mix:referenceable')) {
  533. throw new ConstraintViolationException('You can only change the uuid of newly created nodes that have "referenceable" mixin.');
  534. }
  535. if ($validate) {
  536. if (is_array($value)) {
  537. foreach ($value as $key => $v) {
  538. if (null === $v) {
  539. unset($value[$key]);
  540. }
  541. }
  542. }
  543. $types = $this->getMixinNodeTypes();
  544. array_push($types, $this->getPrimaryNodeType());
  545. if (null !== $value) {
  546. $exception = null;
  547. foreach ($types as $nt) {
  548. /** @var $nt NodeType */
  549. try {
  550. $nt->canSetProperty($name, $value, true);
  551. $exception = null;
  552. break; // exit loop, we found a valid definition
  553. } catch (RepositoryException $e) {
  554. if (null === $exception) {
  555. $exception = $e;
  556. }
  557. }
  558. }
  559. if (null !== $exception) {
  560. $types = 'Primary type '.$this->primaryType;
  561. if (isset($this->properties['jcr:mixinTypes'])) {
  562. $types .= ', mixins '.implode(',', $this->getPropertyValue('jcr:mixinTypes', PropertyType::STRING));
  563. }
  564. $msg = sprintf('Can not set property %s on node %s. Node types do not allow for this: %s', $name, $this->path, $types);
  565. throw new ConstraintViolationException($msg, 0, $exception);
  566. }
  567. } else {
  568. // $value is null for property removal
  569. // if any type forbids, throw exception
  570. foreach ($types as $nt) {
  571. /** @var $nt \Jackalope\NodeType\NodeType */
  572. $nt->canRemoveProperty($name, true);
  573. }
  574. }
  575. }
  576. //try to get a namespace for the set property
  577. if (strpos($name, ':') !== false) {
  578. list($prefix) = explode(':', $name);
  579. //Check if the namespace exists. If not, throw an NamespaceException
  580. $this->session->getNamespaceURI($prefix);
  581. }
  582. if (is_null($value)) {
  583. if (isset($this->properties[$name])) {
  584. $this->properties[$name]->remove();
  585. }
  586. return null;
  587. }
  588. // if the property is the UUID, then register the UUID against the path
  589. // of this node.
  590. if ('jcr:uuid' === $name) {
  591. $this->objectManager->registerUuid($value, $this->getPath());
  592. }
  593. return $this->_setProperty($name, $value, $type, false);
  594. }
  595. /**
  596. * {@inheritDoc}
  597. *
  598. * @return NodeInterface the node at relPath
  599. *
  600. * @throws InvalidItemStateException
  601. *
  602. * @api
  603. */
  604. public function getNode($relPath)
  605. {
  606. $this->checkState();
  607. $relPath = (string) $relPath;
  608. if ('' === $relPath || '/' === $relPath[0]) {
  609. throw new PathNotFoundException("$relPath is not a relative path");
  610. }
  611. try {
  612. $node = $this->objectManager->getNodeByPath(PathHelper::absolutizePath($relPath, $this->path));
  613. } catch (ItemNotFoundException $e) {
  614. throw new PathNotFoundException($e->getMessage(), $e->getCode(), $e);
  615. }
  616. return $node;
  617. }
  618. /**
  619. * {@inheritDoc}
  620. *
  621. * @return \Iterator<string, NodeInterface> over all (matching) child Nodes implementing <b>SeekableIterator</b>
  622. * and <b>Countable</b>. Keys are the Node names.
  623. *
  624. * @api
  625. */
  626. public function getNodes($nameFilter = null, $typeFilter = null)
  627. {
  628. $this->checkState();
  629. $names = self::filterNames($nameFilter, $this->nodes);
  630. $result = [];
  631. if (count($names)) {
  632. $paths = [];
  633. foreach ($names as $name) {
  634. $paths[] = PathHelper::absolutizePath($name, $this->path);
  635. }
  636. $nodes = $this->objectManager->getNodesByPath($paths, Node::class, $typeFilter);
  637. // OPTIMIZE if we lazy-load in ObjectManager we should not do this loop
  638. foreach ($nodes as $node) {
  639. $result[$node->getName()] = $node;
  640. }
  641. }
  642. return new ArrayIterator($result);
  643. }
  644. /**
  645. * {@inheritDoc}
  646. *
  647. * @return \Iterator<string> over all child node names
  648. *
  649. * @api
  650. */
  651. public function getNodeNames($nameFilter = null, $typeFilter = null)
  652. {
  653. $this->checkState();
  654. if (null !== $typeFilter) {
  655. return $this->objectManager->filterChildNodeNamesByType($this, $nameFilter, $typeFilter);
  656. }
  657. $names = self::filterNames($nameFilter, $this->nodes);
  658. return new ArrayIterator($names);
  659. }
  660. /**
  661. * {@inheritDoc}
  662. *
  663. * @return \PHPCR\PropertyInterface the property at relPath
  664. *
  665. * @throws InvalidItemStateException
  666. *
  667. * @api
  668. */
  669. public function getProperty($relPath)
  670. {
  671. $this->checkState();
  672. if (false === strpos($relPath, '/')) {
  673. if (!isset($this->properties[$relPath])) {
  674. throw new PathNotFoundException("Property $relPath in ".$this->path);
  675. }
  676. if ($this->properties[$relPath]->isDeleted()) {
  677. throw new PathNotFoundException("Property '$relPath' of " . $this->path . ' is deleted');
  678. }
  679. return $this->properties[$relPath];
  680. }
  681. return $this->session->getProperty($this->getChildPath($relPath));
  682. }
  683. /**
  684. * This method is only meant for the transport to be able to still build a
  685. * store request for afterwards deleted nodes to support the operationslog.
  686. *
  687. * @return Property[] with just the jcr:primaryType property in it
  688. *
  689. * @see \Jackalope\Transport\WritingInterface::storeNodes
  690. *
  691. * @throws InvalidItemStateException
  692. * @throws RepositoryException
  693. *
  694. * @private
  695. */
  696. public function getPropertiesForStoreDeletedNode()
  697. {
  698. if (! $this->isDeleted()) {
  699. throw new InvalidItemStateException('You are not supposed to call this on a not deleted node');
  700. }
  701. $myProperty = $this->properties['jcr:primaryType'];
  702. $myProperty->setClean();
  703. $path = $this->getChildPath('jcr:primaryType');
  704. $property = $this->factory->get(
  705. 'Property',
  706. [['type' => $myProperty->getType(), 'value' => $myProperty->getValue()],
  707. $path,
  708. $this->session,
  709. $this->objectManager
  710. ]
  711. );
  712. $myProperty->setDeleted();
  713. return ['jcr:primaryType' => $property];
  714. }
  715. /**
  716. * {@inheritDoc}
  717. *
  718. * @return mixed the value of the property with $name
  719. *
  720. * @throws InvalidItemStateException
  721. * @throws \InvalidArgumentException
  722. *
  723. * @api
  724. */
  725. public function getPropertyValue($name, $type = null)
  726. {
  727. $this->checkState();
  728. $val = $this->getProperty($name)->getValue();
  729. if (null !== $type) {
  730. $val = $this->valueConverter->convertType($val, $type);
  731. }
  732. return $val;
  733. }
  734. /**
  735. * {@inheritDoc}
  736. *
  737. * @return mixed the value of the property at $relPath or $defaultValue
  738. *
  739. * @throws \InvalidArgumentException
  740. *
  741. * @throws InvalidItemStateException
  742. * @throws PathNotFoundException
  743. * @throws ValueFormatException
  744. *
  745. * @api
  746. */
  747. public function getPropertyValueWithDefault($relPath, $defaultValue)
  748. {
  749. if ($this->hasProperty($relPath)) {
  750. return $this->getPropertyValue($relPath);
  751. }
  752. return $defaultValue;
  753. }
  754. /**
  755. * {@inheritDoc}
  756. *
  757. * @return \Iterator<string, \PHPCR\PropertyInterface> implementing <b>SeekableIterator</b> and
  758. * <b>Countable</b>. Keys are the property names.
  759. *
  760. * @api
  761. */
  762. public function getProperties($nameFilter = null)
  763. {
  764. $this->checkState();
  765. //OPTIMIZE: lazy iterator?
  766. $names = self::filterNames($nameFilter, array_keys($this->properties));
  767. $result = [];
  768. foreach ($names as $name) {
  769. //we know for sure the properties exist, as they come from the
  770. // array keys of the array we are accessing
  771. $result[$name] = $this->properties[$name];
  772. }
  773. return new ArrayIterator($result);
  774. }
  775. /**
  776. * {@inheritDoc}
  777. *
  778. * @return array<string, mixed> keys are the property names, values the corresponding
  779. * property value (or array of values in case of multi-valued properties)
  780. * If $dereference is false, reference properties are uuid strings and
  781. * path properties path strings instead of the referenced node instances
  782. *
  783. * @throws \InvalidArgumentException
  784. * @throws InvalidItemStateException
  785. * @throws ValueFormatException
  786. * @throws ItemNotFoundException
  787. *
  788. * @api
  789. */
  790. public function getPropertiesValues($nameFilter = null, $dereference = true)
  791. {
  792. $this->checkState();
  793. // OPTIMIZE: do not create properties in constructor, go over array here
  794. $names = self::filterNames($nameFilter, array_keys($this->properties));
  795. $result = [];
  796. foreach ($names as $name) {
  797. //we know for sure the properties exist, as they come from the
  798. // array keys of the array we are accessing
  799. $type = $this->properties[$name]->getType();
  800. if (! $dereference &&
  801. (PropertyType::REFERENCE === $type
  802. || PropertyType::WEAKREFERENCE === $type
  803. || PropertyType::PATH === $type)
  804. ) {
  805. $result[$name] = $this->properties[$name]->getString();
  806. } else {
  807. // OPTIMIZE: collect the paths and call objectmanager->getNodesByPath once
  808. $result[$name] = $this->properties[$name]->getValue();
  809. }
  810. }
  811. return $result;
  812. }
  813. /**
  814. * {@inheritDoc}
  815. *
  816. * @return ItemInterface the primary child item
  817. *
  818. * @api
  819. */
  820. public function getPrimaryItem()
  821. {
  822. try {
  823. $primary_item = null;
  824. $item_name = $this->getPrimaryNodeType()->getPrimaryItemName();
  825. if ($item_name !== null) {
  826. $primary_item = $this->session->getItem($this->path . '/' . $item_name);
  827. }
  828. } catch (Exception $ex) {
  829. throw new RepositoryException("An error occured while reading the primary item of the node '{$this->path}': " . $ex->getMessage());
  830. }
  831. if ($primary_item === null) {
  832. throw new ItemNotFoundException("No primary item found for node '{$this->path}'");
  833. }
  834. return $primary_item;
  835. }
  836. /**
  837. * @return string a universally unique id.
  838. */
  839. protected function generateUuid()
  840. {
  841. return UUIDHelper::generateUUID();
  842. }
  843. /**
  844. * {@inheritDoc}
  845. *
  846. * @return string the identifier of this node
  847. *
  848. * @throws \InvalidArgumentException
  849. *
  850. * @throws AccessDeniedException
  851. * @throws InvalidItemStateException
  852. * @throws ItemNotFoundException
  853. * @throws LockException
  854. * @throws NamespaceException
  855. * @throws ConstraintViolationException
  856. * @throws ValueFormatException
  857. * @throws VersionException
  858. * @throws PathNotFoundException
  859. *
  860. * @api
  861. */
  862. public function getIdentifier()
  863. {
  864. $this->checkState();
  865. if ($this->isNodeType('mix:referenceable')) {
  866. if (empty($this->properties['jcr:uuid'])) {
  867. $this->setProperty('jcr:uuid', $this->generateUuid());
  868. }
  869. return $this->getPropertyValue('jcr:uuid');
  870. }
  871. return $this->getPath();
  872. }
  873. /**
  874. * {@inheritDoc}
  875. *
  876. * @return int the index of this node within the ordered set of its
  877. * same-name sibling nodes
  878. *
  879. * @api
  880. */
  881. public function getIndex()
  882. {
  883. $this->checkState();
  884. return $this->index;
  885. }
  886. /**
  887. * {@inheritDoc}
  888. *
  889. * @return \Iterator<string, \PHPCR\PropertyInterface> implementing <b>SeekableIterator</b> and
  890. * <b>Countable</b>. Keys are the property names.
  891. *
  892. * @api
  893. */
  894. public function getReferences($name = null)
  895. {
  896. $this->checkState();
  897. return $this->objectManager->getReferences($this->path, $name);
  898. }
  899. /**
  900. * {@inheritDoc}
  901. *
  902. * @return \Iterator<string, \PHPCR\PropertyInterface> implementing <b>SeekableIterator</b> and
  903. * <b>Countable</b>. Keys are the property names.
  904. * @api
  905. */
  906. public function getWeakReferences($name = null)
  907. {
  908. $this->checkState();
  909. return $this->objectManager->getWeakReferences($this->path, $name);
  910. }
  911. /**
  912. * {@inheritDoc}
  913. *
  914. * @return bool true if a node exists at relPath; false otherwise
  915. *
  916. * @api
  917. */
  918. public function hasNode($relPath)
  919. {
  920. $this->checkState();
  921. if (false === strpos($relPath, '/')) {
  922. return array_search($relPath, $this->nodes) !== false;
  923. }
  924. if (! strlen($relPath) || $relPath[0] === '/') {
  925. throw new InvalidArgumentException("'$relPath' is not a relative path");
  926. }
  927. return $this->session->nodeExists($this->getChildPath($relPath));
  928. }
  929. /**
  930. * {@inheritDoc}
  931. *
  932. * @return bool true if a property exists at relPath; false otherwise
  933. *
  934. * @api
  935. */
  936. public function hasProperty($relPath)
  937. {
  938. $this->checkState();
  939. if (false === strpos($relPath, '/')) {
  940. return isset($this->properties[$relPath]);
  941. }
  942. if (! strlen($relPath) || $relPath[0] === '/') {
  943. throw new InvalidArgumentException("'$relPath' is not a relative path");
  944. }
  945. return $this->session->propertyExists($this->getChildPath($relPath));
  946. }
  947. /**
  948. * {@inheritDoc}
  949. *
  950. * @return bool true if this node has one or more child nodes; false
  951. * otherwise
  952. *
  953. * @api
  954. */
  955. public function hasNodes()
  956. {
  957. $this->checkState();
  958. return count($this->nodes) !== 0;
  959. }
  960. /**
  961. * {@inheritDoc}
  962. *
  963. * @return bool true if this node has one or more properties; false
  964. * otherwise
  965. *
  966. * @api
  967. */
  968. public function hasProperties()
  969. {
  970. $this->checkState();
  971. return count($this->properties) !== 0;
  972. }
  973. /**
  974. * {@inheritDoc}
  975. *
  976. * @return NodeTypeInterface a NodeType object
  977. *
  978. * @api
  979. */
  980. public function getPrimaryNodeType()
  981. {
  982. $this->checkState();
  983. $ntm = $this->session->getWorkspace()->getNodeTypeManager();
  984. return $ntm->getNodeType($this->primaryType);
  985. }
  986. /**
  987. * {@inheritDoc}
  988. *
  989. * @return NodeTypeInterface[] an array of mixin node types
  990. *
  991. * @api
  992. */
  993. public function getMixinNodeTypes()
  994. {
  995. $this->checkState();
  996. if (!isset($this->properties['jcr:mixinTypes'])) {
  997. return [];
  998. }
  999. $res = [];
  1000. $ntm = $this->session->getWorkspace()->getNodeTypeManager();
  1001. foreach ($this->properties['jcr:mixinTypes']->getValue() as $type) {
  1002. $res[] = $ntm->getNodeType($type);
  1003. }
  1004. return $res;
  1005. }
  1006. /**
  1007. * {@inheritDoc}
  1008. *
  1009. * @return bool true if this node is of the specified primary node type
  1010. * or mixin type, or a subtype thereof. Returns false otherwise.
  1011. * @api
  1012. */
  1013. public function isNodeType($nodeTypeName)
  1014. {
  1015. $this->checkState();
  1016. // is it the primary type?
  1017. if ($this->primaryType === $nodeTypeName) {
  1018. return true;
  1019. }
  1020. // is it one of the mixin types?
  1021. if (isset($this->properties['jcr:mixinTypes'])) {
  1022. if (in_array($nodeTypeName, $this->properties["jcr:mixinTypes"]->getValue())) {
  1023. return true;
  1024. }
  1025. }
  1026. $ntm = $this->session->getWorkspace()->getNodeTypeManager();
  1027. // is the primary type a subtype of the type?
  1028. if ($ntm->getNodeType($this->primaryType)->isNodeType($nodeTypeName)) {
  1029. return true;
  1030. }
  1031. // if there are no mixin types, then we now know this node is not of that type
  1032. if (! isset($this->properties["jcr:mixinTypes"])) {
  1033. return false;
  1034. }
  1035. // is it an ancestor of any of the mixin types?
  1036. foreach ($this->properties['jcr:mixinTypes'] as $mixin) {
  1037. if ($ntm->getNodeType($mixin)->isNodeType($nodeTypeName)) {
  1038. return true;
  1039. }
  1040. }
  1041. return false;
  1042. }
  1043. /**
  1044. * Changes the primary node type of this node to nodeTypeName.
  1045. *
  1046. * {@inheritDoc}
  1047. *
  1048. * Jackalope only validates type conflicts on save.
  1049. *
  1050. * @throws InvalidItemStateException
  1051. *
  1052. * @api
  1053. */
  1054. public function setPrimaryType($nodeTypeName)
  1055. {
  1056. $this->checkState();
  1057. throw new NotImplementedException('Write');
  1058. }
  1059. /**
  1060. * {@inheritDoc}
  1061. *
  1062. * Jackalope validates type conflicts only on save, not immediately.
  1063. * It is possible to add mixin types after the first save.
  1064. *
  1065. * @api
  1066. */
  1067. public function addMixin($mixinName)
  1068. {
  1069. // Check if mixinName exists as a mixin type
  1070. $typemgr = $this->session->getWorkspace()->getNodeTypeManager();
  1071. $nodeType = $typemgr->getNodeType($mixinName);
  1072. if (! $nodeType->isMixin()) {
  1073. throw new ConstraintViolationException("Trying to add a mixin '$mixinName' that is a primary type");
  1074. }
  1075. $this->checkState();
  1076. // TODO handle LockException & VersionException cases
  1077. if ($this->hasProperty('jcr:mixinTypes')) {
  1078. if (!in_array($mixinName, $this->properties['jcr:mixinTypes']->getValue())) {
  1079. $this->properties['jcr:mixinTypes']->addValue($mixinName);
  1080. $this->setModified();
  1081. }
  1082. } else {
  1083. $this->setProperty('jcr:mixinTypes', [$mixinName], PropertyType::NAME);
  1084. $this->setModified();
  1085. }
  1086. }
  1087. /**
  1088. * {@inheritDoc}
  1089. *
  1090. * @throws InvalidItemStateException
  1091. *
  1092. * @throws \InvalidArgumentException
  1093. * @throws AccessDeniedException
  1094. * @throws ItemNotFoundException
  1095. * @throws PathNotFoundException
  1096. * @throws NamespaceException
  1097. * @throws ValueFormatException
  1098. *
  1099. * @api
  1100. */
  1101. public function removeMixin($mixinName)
  1102. {
  1103. $this->checkState();
  1104. // check if node type is assigned
  1105. if (! $this->hasProperty('jcr:mixinTypes')) {
  1106. throw new NoSuchNodeTypeException("Node does not have type $mixinName");
  1107. }
  1108. $mixins = $this->getPropertyValue('jcr:mixinTypes');
  1109. $key = array_search($mixinName, $mixins);
  1110. if (false === $key) {
  1111. throw new NoSuchNodeTypeException("Node does not have type $mixinName");
  1112. }
  1113. unset($mixins[$key]);
  1114. $this->setProperty('jcr:mixinTypes', $mixins); // might be empty array which is fine
  1115. }
  1116. /**
  1117. * {@inheritDoc}
  1118. *
  1119. * @throws \InvalidArgumentException
  1120. * @throws AccessDeniedException
  1121. * @throws InvalidItemStateException
  1122. * @throws ItemNotFoundException
  1123. * @throws NamespaceException
  1124. * @throws PathNotFoundException
  1125. * @throws ValueFormatException
  1126. *
  1127. * @api
  1128. */
  1129. public function setMixins(array $mixinNames)
  1130. {
  1131. $toRemove = [];
  1132. if ($this->hasProperty('jcr:mixinTypes')) {
  1133. foreach ($this->getPropertyValue('jcr:mixinTypes') as $mixin) {
  1134. if (false !== $key = array_search($mixin, $mixinNames)) {
  1135. unset($mixinNames[$key]);
  1136. } else {
  1137. $toRemove[] = $mixin;
  1138. }
  1139. }
  1140. }
  1141. if (! (count($toRemove) || count($mixinNames))) {
  1142. return; // nothing to do
  1143. }
  1144. // make sure the new types actually exist before we add anything
  1145. $ntm = $this->session->getWorkspace()->getNodeTypeManager();
  1146. foreach ($mixinNames as $mixinName) {
  1147. $nodeType = $ntm->getNodeType($mixinName);
  1148. if (! $nodeType->isMixin()) {
  1149. throw new ConstraintViolationException("Trying to add a mixin '$mixinName' that is a primary type");
  1150. }
  1151. }
  1152. foreach ($mixinNames as $type) {
  1153. $this->addMixin($type);
  1154. }
  1155. foreach ($toRemove as $type) {
  1156. $this->removeMixin($type);
  1157. }
  1158. }
  1159. /**
  1160. * {@inheritDoc}
  1161. *
  1162. * @return bool true if the specified mixin node type, mixinName, can be
  1163. * added to this node; false otherwise
  1164. *
  1165. * @throws InvalidItemStateException
  1166. *
  1167. * @api
  1168. */
  1169. public function canAddMixin($mixinName)
  1170. {
  1171. $this->checkState();
  1172. throw new NotImplementedException('Write');
  1173. }
  1174. /**
  1175. * {@inheritDoc}
  1176. *
  1177. * @return NodeDefinitionInterface a NodeDefinition object
  1178. *
  1179. * @api
  1180. */
  1181. public function getDefinition()
  1182. {
  1183. $this->checkState();
  1184. if ('rep:root' === $this->primaryType) {
  1185. throw new NotImplementedException('what is the definition of the root node?');
  1186. }
  1187. if (empty($this->definition)) {
  1188. $this->definition = $this->findItemDefinition(function (NodeTypeInterface $nt) {
  1189. return $nt->getChildNodeDefinitions();
  1190. });
  1191. }
  1192. return $this->definition;
  1193. }
  1194. /**
  1195. * {@inheritDoc}
  1196. *
  1197. * @api
  1198. */
  1199. public function update($srcWorkspace)
  1200. {
  1201. $this->checkState();
  1202. if ($this->isNew()) {
  1203. //no node in workspace
  1204. return;
  1205. }
  1206. $this->getSession()->getTransport()->updateNode($this, $srcWorkspace);
  1207. $this->setDirty();
  1208. $this->setChildrenDirty();
  1209. }
  1210. /**
  1211. * {@inheritDoc}
  1212. *
  1213. * @return string the absolute path to the corresponding node
  1214. *
  1215. * @throws InvalidItemStateException
  1216. *
  1217. * @api
  1218. */
  1219. public function getCorrespondingNodePath($workspaceName)
  1220. {
  1221. $this->checkState();
  1222. return $this->getSession()
  1223. ->getTransport()
  1224. ->getNodePathForIdentifier($this->getIdentifier(), $workspaceName);
  1225. }
  1226. /**
  1227. * {@inheritDoc}
  1228. *
  1229. * @return \Iterator<string, NodeInterface> implementing <b>SeekableIterator</b> and
  1230. * <b>Countable</b>. Keys are the Node names.
  1231. * @api
  1232. */
  1233. public function getSharedSet()
  1234. {
  1235. $this->checkState();
  1236. throw new NotImplementedException();
  1237. }
  1238. /**
  1239. * {@inheritDoc}
  1240. *
  1241. * @return void
  1242. *
  1243. * @throws InvalidItemStateException
  1244. *
  1245. * @api
  1246. */
  1247. public function removeSharedSet()
  1248. {
  1249. $this->checkState();
  1250. $this->setModified();
  1251. throw new NotImplementedException('Write');
  1252. }
  1253. /**
  1254. * {@inheritDoc}
  1255. *
  1256. * @throws InvalidItemStateException
  1257. *
  1258. * @api
  1259. */
  1260. public function removeShare()
  1261. {
  1262. $this->checkState();
  1263. $this->setModified();
  1264. throw new NotImplementedException('Write');
  1265. }
  1266. /**
  1267. * {@inheritDoc}
  1268. *
  1269. * @return bool
  1270. *
  1271. * @api
  1272. */
  1273. public function isCheckedOut()
  1274. {
  1275. $this->checkState();
  1276. $workspace = $this->session->getWorkspace();
  1277. $versionManager = $workspace->getVersionManager();
  1278. return $versionManager->isCheckedOut($this->getPath());
  1279. }
  1280. /**
  1281. * {@inheritDoc}
  1282. *
  1283. * @return bool
  1284. *
  1285. * @api
  1286. */
  1287. public function isLocked()
  1288. {
  1289. $this->checkState();
  1290. throw new NotImplementedException();
  1291. }
  1292. /**
  1293. * {@inheritDoc}
  1294. *
  1295. * @throws InvalidItemStateException
  1296. *
  1297. * @api
  1298. */
  1299. public function followLifecycleTransition($transition)
  1300. {
  1301. $this->checkState();
  1302. $this->setModified();
  1303. throw new NotImplementedException('Write');
  1304. }
  1305. /**
  1306. * {@inheritDoc}
  1307. *
  1308. * @return string[]
  1309. *
  1310. * @throws InvalidItemStateException
  1311. *
  1312. * @api
  1313. */
  1314. public function getAllowedLifecycleTransitions()
  1315. {
  1316. $this->checkState();
  1317. throw new NotImplementedException('Write');
  1318. }
  1319. /**
  1320. * Refresh this node
  1321. *
  1322. * {@inheritDoc}
  1323. *
  1324. * This is also called internally to refresh when the node is accessed in
  1325. * state DIRTY.
  1326. *
  1327. * @see Item::checkState
  1328. */
  1329. protected function refresh($keepChanges, $internal = false)
  1330. {
  1331. if (! $internal && $this->isDeleted()) {
  1332. throw new InvalidItemStateException('This item has been removed and can not be refreshed');
  1333. }
  1334. $deleted = false;
  1335. // Get properties and children from backend
  1336. try {
  1337. $json = $this->objectManager->getTransport()->getNode(
  1338. is_null($this->oldPath)
  1339. ? $this->path
  1340. : $this->oldPath
  1341. );
  1342. } catch (ItemNotFoundException $ex) {
  1343. // The node was deleted in another session
  1344. if (! $this->objectManager->purgeDisappearedNode($this->path, $keepChanges)) {
  1345. throw new LogicException($this->path . " should be purged and not kept");
  1346. }
  1347. $keepChanges = false; // delete never keeps changes
  1348. if (! $internal) {
  1349. // this is not an internal update
  1350. $deleted = true;
  1351. }
  1352. // continue with empty data, parseData will notify all cached
  1353. // children and all properties that we are removed
  1354. $json = [];
  1355. }
  1356. $this->parseData($json, true, $keepChanges);
  1357. if ($deleted) {
  1358. $this->setDeleted();
  1359. }
  1360. }
  1361. /**
  1362. * Remove this node
  1363. *
  1364. * {@inheritDoc}
  1365. *
  1366. * A jackalope node needs to notify the parent node about this if it is
  1367. * cached, in addition to \PHPCR\ItemInterface::remove()
  1368. *
  1369. * @uses Node::unsetChildNode()
  1370. *
  1371. * @api
  1372. */
  1373. public function remove()
  1374. {
  1375. $this->checkState();
  1376. $parent = $this->getParent();
  1377. $parentNodeType = $parent->getPrimaryNodeType();
  1378. //will throw a ConstraintViolationException if this node can't be removed
  1379. $parentNodeType->canRemoveNode($this->getName(), true);
  1380. if ($parent) {
  1381. $parent->unsetChildNode($this->name, true);
  1382. }
  1383. // once we removed ourselves, $this->getParent() won't work anymore. do this last
  1384. parent::remove();
  1385. }
  1386. /**
  1387. * Removes the reference in the internal node storage
  1388. *
  1389. * @param string $name the name of the child node to unset
  1390. * @param bool $check whether a state check should be done - set to false
  1391. * during internal update operations
  1392. *
  1393. * @throws ItemNotFoundException If there is no child with $name
  1394. * @throws InvalidItemStateException
  1395. *
  1396. * @private
  1397. */
  1398. public function unsetChildNode($name, $check)
  1399. {
  1400. if ($check) {
  1401. $this->checkState();
  1402. }
  1403. $key = array_search($name, $this->nodes);
  1404. if ($key === false) {
  1405. if (! $check) {
  1406. // inside a refresh operation
  1407. return;
  1408. }
  1409. throw new ItemNotFoundException("Could not remove child node because it's already gone");
  1410. }
  1411. unset($this->nodes[$key]);
  1412. if (null !== $this->originalNodesOrder) {
  1413. $this->originalNodesOrder = array_flip($this->originalNodesOrder);
  1414. unset($this->originalNodesOrder[$name]);
  1415. $this->originalNodesOrder = array_flip($this->originalNodesOrder);
  1416. }
  1417. }
  1418. /**
  1419. * Adds child node to this node for internal reference
  1420. *
  1421. * @param NodeInterface $node The name of the child node
  1422. * @param boolean $check whether to check state
  1423. * @param string $name is used in cases where $node->getName would not return the correct name (during move operation)
  1424. *
  1425. * @throws InvalidItemStateException
  1426. * @throws RepositoryException
  1427. *
  1428. * @private
  1429. */
  1430. public function addChildNode(NodeInterface $node, $check, $name = null)
  1431. {
  1432. if ($check) {
  1433. $this->checkState();
  1434. }
  1435. if (is_null($name)) {
  1436. $name = $node->getName();
  1437. }
  1438. $nt = $this->getPrimaryNodeType();
  1439. //will throw a ConstraintViolationException if this node can't be added
  1440. $nt->canAddChildNode($name, $node->getPrimaryNodeType()->getName(), true);
  1441. // TODO: same name siblings
  1442. $this->nodes[] = $name;
  1443. if (null !== $this->originalNodesOrder) {
  1444. $this->originalNodesOrder[] = $name;
  1445. }
  1446. }
  1447. /**
  1448. * Removes the reference in the internal node storage
  1449. *
  1450. * @param string $name the name of the property to unset.
  1451. *
  1452. * @throws ItemNotFoundException If this node has no property with name $name
  1453. * @throws InvalidItemStateException
  1454. * @throws RepositoryException
  1455. *
  1456. * @private
  1457. */
  1458. public function unsetProperty($name)
  1459. {
  1460. $this->checkState();
  1461. $this->setModified();
  1462. if (!array_key_exists($name, $this->properties)) {
  1463. throw new ItemNotFoundException('Implementation Error: Could not remove property from node because it is already gone');
  1464. }
  1465. $this->deletedProperties[$name] = $this->properties[$name];
  1466. unset($this->properties[$name]);
  1467. }
  1468. /**
  1469. * In addition to calling parent method, tell all properties and clean deletedProperties
  1470. */
  1471. public function confirmSaved()
  1472. {
  1473. foreach ($this->properties as $property) {
  1474. if ($property->isModified() || $property->isNew()) {
  1475. $property->confirmSaved();
  1476. }
  1477. }
  1478. $this->deletedProperties = [];
  1479. parent::confirmSaved();
  1480. }
  1481. /**
  1482. * In addition to calling parent method, tell all properties
  1483. */
  1484. public function setPath($path, $move = false)
  1485. {
  1486. parent::setPath($path, $move);
  1487. foreach ($this->properties as $property) {
  1488. $property->setPath($path.'/'.$property->getName(), $move);
  1489. }
  1490. }
  1491. /**
  1492. * Make sure $p is an absolute path
  1493. *
  1494. * If its a relative path, prepend the path to this node, otherwise return as is
  1495. *
  1496. * @param string $p the relative or absolute property or node path
  1497. *
  1498. * @return string the absolute path to this item, with relative paths resolved against the current node
  1499. */
  1500. protected function getChildPath($p)
  1501. {
  1502. if ('' == $p) {
  1503. throw new InvalidArgumentException("Name can not be empty");
  1504. }
  1505. if ($p[0] == '/') {
  1506. return $p;
  1507. }
  1508. //relative path, combine with base path for this node
  1509. $path = $this->path === '/' ? '/' : $this->path.'/';
  1510. return $path . $p;
  1511. }
  1512. /**
  1513. * Filter the list of names according to the filter expression / array
  1514. *
  1515. * @param string|array $filter according to getNodes|getProperties
  1516. * @param array $names list of names to filter
  1517. *
  1518. * @return array the names in $names that match the filter
  1519. */
  1520. protected static function filterNames($filter, $names)
  1521. {
  1522. if ($filter !== null) {
  1523. $filtered = [];
  1524. $filter = (array) $filter;
  1525. foreach ($filter as $k => $f) {
  1526. $f = trim($f);
  1527. $filter[$k] = strtr($f, [
  1528. '*'=>'.*', //wildcard
  1529. '.' => '\\.', //escape regexp
  1530. '\\' => '\\\\',
  1531. '{' => '\\{',
  1532. '}' => '\\}',
  1533. '(' => '\\(',
  1534. ')' => '\\)',
  1535. '+' => '\\+',
  1536. '^' => '\\^',
  1537. '$' => '\\$'
  1538. ]);
  1539. }
  1540. foreach ($names as $name) {
  1541. foreach ($filter as $f) {
  1542. if (preg_match('/^'.$f.'$/', $name)) {
  1543. $filtered[] = $name;
  1544. }
  1545. }
  1546. }
  1547. } else {
  1548. $filtered = $names;
  1549. }
  1550. return $filtered;
  1551. }
  1552. /**
  1553. * Provide Traversable interface: redirect to getNodes with no filter
  1554. *
  1555. * @return Iterator over all child nodes
  1556. * @throws RepositoryException
  1557. */
  1558. #[\ReturnTypeWillChange]
  1559. public function getIterator()
  1560. {
  1561. $this->checkState();
  1562. return $this->getNodes();
  1563. }
  1564. /**
  1565. * Implement really setting the property without any notification.
  1566. *
  1567. * Implement the setProperty, but also used from constructor or in refresh,
  1568. * when the backend has a new property that is not yet loaded in memory.
  1569. *
  1570. * @param string $name
  1571. * @param mixed $value
  1572. * @param string $type
  1573. * @param boolean $internal whether we are setting this node through api or internally
  1574. *
  1575. * @return Property
  1576. *
  1577. * @throws InvalidArgumentException
  1578. * @throws LockException
  1579. * @throws ConstraintViolationException
  1580. * @throws RepositoryException
  1581. * @throws UnsupportedRepositoryOperationException
  1582. * @throws ValueFormatException
  1583. * @throws VersionException
  1584. *
  1585. * @see Node::setProperty
  1586. * @see Node::refresh
  1587. * @see Node::__construct
  1588. */
  1589. protected function _setProperty($name, $value, $type, $internal)
  1590. {
  1591. if ($name === '' || false !== strpos($name, '/')) {
  1592. throw new InvalidArgumentException("The name '$name' is no valid property name");
  1593. }
  1594. if (!isset($this->properties[$name])) {
  1595. $path = $this->getChildPath($name);
  1596. $property = $this->factory->get(
  1597. Property::class,
  1598. [
  1599. ['type' => $type, 'value' => $value],
  1600. $path,
  1601. $this->session,
  1602. $this->objectManager,
  1603. ! $internal
  1604. ]
  1605. );
  1606. $this->properties[$name] = $property;
  1607. if (! $internal) {
  1608. $this->setModified();
  1609. }
  1610. } else {
  1611. if ($internal) {
  1612. $this->properties[$name]->_setValue($value, $type);
  1613. if ($this->properties[$name]->isDirty()) {
  1614. $this->properties[$name]->setClean();
  1615. }
  1616. } else {
  1617. $this->properties[$name]->setValue($value, $type);
  1618. }
  1619. }
  1620. return $this->properties[$name];
  1621. }
  1622. /**
  1623. * Overwrite to set the properties dirty as well.
  1624. *
  1625. * @private
  1626. */
  1627. public function setDirty($keepChanges = false, $targetState = false)
  1628. {
  1629. parent::setDirty($keepChanges, $targetState);
  1630. foreach ($this->properties as $property) {
  1631. if ($keepChanges && self::STATE_NEW !== $property->getState()) {
  1632. // if we want to keep changes, we do not want to set new properties dirty.
  1633. $property->setDirty($keepChanges, $targetState);
  1634. }
  1635. }
  1636. }
  1637. /**
  1638. * Mark all cached children as dirty.
  1639. *
  1640. * @private
  1641. */
  1642. public function setChildrenDirty()
  1643. {
  1644. foreach ($this->objectManager->getCachedDescendants($this->getPath()) as $childNode) {
  1645. $childNode->setDirty();
  1646. }
  1647. }
  1648. /**
  1649. * In addition to set this item deleted, set all properties to deleted.
  1650. *
  1651. * They will be automatically deleted by the backend, but the user might
  1652. * still have a reference to one of the property objects.
  1653. *
  1654. * @private
  1655. */
  1656. public function setDeleted()
  1657. {
  1658. parent::setDeleted();
  1659. foreach ($this->properties as $property) {
  1660. $property->setDeleted(); // not all properties are tracked in objectmanager
  1661. }
  1662. }
  1663. /**
  1664. * {@inheritDoc}
  1665. *
  1666. * Additionally, notifies all properties of this node. Child nodes are not
  1667. * notified, it is the job of the ObjectManager to know which nodes are
  1668. * cached and notify them.
  1669. */
  1670. public function beginTransaction()
  1671. {
  1672. parent::beginTransaction();
  1673. // Notify the children properties
  1674. foreach ($this->properties as $prop) {
  1675. $prop->beginTransaction();
  1676. }
  1677. }
  1678. /**
  1679. * {@inheritDoc}
  1680. *
  1681. * Additionally, notifies all properties of this node. Child nodes are not
  1682. * notified, it is the job of the ObjectManager to know which nodes are
  1683. * cached and notify them.
  1684. */
  1685. public function commitTransaction()
  1686. {
  1687. parent::commitTransaction();
  1688. foreach ($this->properties as $prop) {
  1689. $prop->commitTransaction();
  1690. }
  1691. }
  1692. /**
  1693. * {@inheritDoc}
  1694. *
  1695. * Additionally, notifies all properties of this node. Child nodes are not
  1696. * notified, it is the job of the ObjectManager to know which nodes are
  1697. * cached and notify them.
  1698. */
  1699. public function rollbackTransaction()
  1700. {
  1701. parent::rollbackTransaction();
  1702. foreach ($this->properties as $prop) {
  1703. $prop->rollbackTransaction();
  1704. }
  1705. }
  1706. }