vendor/jackalope/jackalope/src/Jackalope/ObjectManager.php line 214

Open in your IDE?
  1. <?php
  2. namespace Jackalope;
  3. use ArrayIterator;
  4. use Exception;
  5. use InvalidArgumentException;
  6. use Jackalope\Transport\NodeTypeFilterInterface;
  7. use Jackalope\Version\Version;
  8. use PHPCR\NamespaceException;
  9. use PHPCR\NodeType\InvalidNodeTypeDefinitionException;
  10. use PHPCR\NodeType\NodeTypeExistsException;
  11. use PHPCR\NodeType\NodeTypeManagerInterface;
  12. use PHPCR\NoSuchWorkspaceException;
  13. use PHPCR\ReferentialIntegrityException;
  14. use PHPCR\SessionInterface;
  15. use PHPCR\NodeInterface;
  16. use PHPCR\PropertyInterface;
  17. use PHPCR\RepositoryException;
  18. use PHPCR\AccessDeniedException;
  19. use PHPCR\ItemNotFoundException;
  20. use PHPCR\ItemExistsException;
  21. use PHPCR\PathNotFoundException;
  22. use PHPCR\Transaction\RollbackException;
  23. use PHPCR\UnsupportedRepositoryOperationException;
  24. use PHPCR\Util\CND\Writer\CndWriter;
  25. use PHPCR\Version\VersionException;
  26. use PHPCR\Version\VersionInterface;
  27. use PHPCR\Util\PathHelper;
  28. use PHPCR\Util\CND\Parser\CndParser;
  29. use Jackalope\Transport\Operation;
  30. use Jackalope\Transport\TransportInterface;
  31. use Jackalope\Transport\PermissionInterface;
  32. use Jackalope\Transport\WritingInterface;
  33. use Jackalope\Transport\NodeTypeManagementInterface;
  34. use Jackalope\Transport\NodeTypeCndManagementInterface;
  35. use Jackalope\Transport\AddNodeOperation;
  36. use Jackalope\Transport\MoveNodeOperation;
  37. use Jackalope\Transport\RemoveNodeOperation;
  38. use Jackalope\Transport\RemovePropertyOperation;
  39. use Jackalope\Transport\VersioningInterface;
  40. use RuntimeException;
  41. /**
  42.  * Implementation specific class that talks to the Transport layer to get nodes
  43.  * and caches every node retrieved to improve performance.
  44.  *
  45.  * For write operations, the object manager acts as the Unit of Work handler:
  46.  * it keeps track which nodes are dirty and updates them with the transport
  47.  * interface.
  48.  *
  49.  * As not all transports have the same capabilities, we do some checks here,
  50.  * but only if the check is not already done at the entry point. For
  51.  * versioning, transactions, locking and so on, the check is done when the
  52.  * respective manager is requested from the session or workspace. As those
  53.  * managers are the only entry points we do not check here again.
  54.  *
  55.  * @license http://www.apache.org/licenses Apache License Version 2.0, January 2004
  56.  * @license http://opensource.org/licenses/MIT MIT License
  57.  *
  58.  * @private
  59.  */
  60. class ObjectManager
  61. {
  62.     /**
  63.      * The factory to instantiate objects
  64.      * @var FactoryInterface
  65.      */
  66.     protected $factory;
  67.     /**
  68.      * @var SessionInterface
  69.      */
  70.     protected $session;
  71.     /**
  72.      * @var TransportInterface
  73.      */
  74.     protected $transport;
  75.     /**
  76.      * Mapping of typename => absolutePath => node or item object.
  77.      *
  78.      * There is no notion of order here. The order is defined by order in the
  79.      * Node::nodes array.
  80.      *
  81.      * @var array
  82.      */
  83.     protected $objectsByPath = [Node::class => []];
  84.     /**
  85.      * Mapping of uuid => absolutePath.
  86.      *
  87.      * Take care never to put a path in here unless there is a node for that
  88.      * path in objectsByPath.
  89.      *
  90.      * @var array
  91.      */
  92.     protected $objectsByUuid = [];
  93.     /**
  94.      * This is an ordered list of all operations to commit to the transport
  95.      * during save. The values are the add, move and remove operation classes.
  96.      *
  97.      * Add, remove and move actions need to be saved in the correct order to avoid
  98.      * i.e. adding something where a node has not yet been moved to.
  99.      *
  100.      * @var Operation[]
  101.      */
  102.     protected $operationsLog = [];
  103.     /**
  104.      * Contains the list of paths that have been added to the workspace in the
  105.      * current session.
  106.      *
  107.      * Keys are the full paths to be added
  108.      *
  109.      * @var AddNodeOperation[]
  110.      */
  111.     protected $nodesAdd = [];
  112.     /**
  113.      * Contains the list of node remove operations for the current session.
  114.      *
  115.      * Keys are the full paths to be removed.
  116.      *
  117.      * Note: Keep in mind that a delete is recursive, but we only have the
  118.      * explicitly deleted paths in this array. We check on deleted parents
  119.      * whenever retrieving a non-cached node.
  120.      *
  121.      * @var RemoveNodeOperation[]
  122.      */
  123.     protected $nodesRemove = [];
  124.     /**
  125.      * Contains the list of property remove operations for the current session.
  126.      *
  127.      * Keys are the full paths of properties to be removed.
  128.      *
  129.      * @var RemovePropertyOperation[]
  130.      */
  131.     protected $propertiesRemove = [];
  132.     /**
  133.      * Contains a list of nodes that where moved during this session.
  134.      *
  135.      * Keys are the source paths, values the move operations containing the
  136.      * target path.
  137.      *
  138.      * The objectsByPath array is updated immediately and any getItem and
  139.      * similar requests are rewritten for the transport layer until save()
  140.      *
  141.      * Only nodes can be moved, not properties.
  142.      *
  143.      * Note: Keep in mind that moving also affects all children of the moved
  144.      * node, but we only have the explicitly moved paths in this array. We
  145.      * check on moved parents whenever retrieving a non-cached node.
  146.      *
  147.      * @var MoveNodeOperation[]
  148.      */
  149.     protected $nodesMove = [];
  150.     /**
  151.      * Create the ObjectManager instance with associated session and transport
  152.      *
  153.      * @param FactoryInterface   $factory   the object factory
  154.      * @param TransportInterface $transport
  155.      * @param SessionInterface   $session
  156.      */
  157.     public function __construct(FactoryInterface $factoryTransportInterface $transportSessionInterface $session)
  158.     {
  159.         $this->factory $factory;
  160.         $this->transport $transport;
  161.         $this->session $session;
  162.     }
  163.     /**
  164.      * Get the node identified by an absolute path.
  165.      *
  166.      * To prevent unnecessary work to be done a cache is filled to only fetch
  167.      * nodes once. To reset a node with the data from the backend, use
  168.      * Node::refresh()
  169.      *
  170.      * Uses the factory to create a Node object.
  171.      *
  172.      * @param string $absPath The absolute path of the node to fetch.
  173.      * @param string $class   The class of node to get. TODO: Is it sane to fetch
  174.      *      data separately for Version and normal Node?
  175.      * @param object $object A (prefetched) object (de-serialized json) from the backend
  176.      *      only to be used if we get child nodes in one backend call
  177.      *
  178.      * @return NodeInterface
  179.      *
  180.      * @throws ItemNotFoundException If nothing is found at that
  181.      *      absolute path
  182.      * @throws RepositoryException If the path is not absolute or not
  183.      *      well-formed
  184.      *
  185.      * @see Session::getNode()
  186.      */
  187.     public function getNodeByPath($absPath$class Node::class, $object null)
  188.     {
  189.         $absPath PathHelper::normalizePath($absPath);
  190.         if (!empty($this->objectsByPath[$class][$absPath])) {
  191.             // Return it from memory if we already have it
  192.             return $this->objectsByPath[$class][$absPath];
  193.         }
  194.         // do this even if we have item in cache, will throw error if path is deleted - sanity check
  195.         $fetchPath $this->getFetchPath($absPath$class);
  196.         if (!$object) {
  197.             // this is the first request, get data from transport
  198.             $object $this->transport->getNode($fetchPath);
  199.         }
  200.         // recursively create nodes for pre-fetched children if fetchDepth was > 1
  201.         foreach ($object as $name => $properties) {
  202.             if (is_object($properties)) {
  203.                 $objVars get_object_vars($properties);
  204.                 $countObjVars count($objVars);
  205.                 // if there's more than one objectvar or just one and this isn't jcr:uuid,
  206.                 // then we assume this child was pre-fetched from the backend completely
  207.                 if ($countObjVars || ($countObjVars === && !isset($objVars['jcr:uuid']))) {
  208.                     try {
  209.                         $parentPath = ('/' === $absPath) ? '/' $absPath '/';
  210.                         $this->getNodeByPath($parentPath $name$class$properties);
  211.                     } catch (ItemNotFoundException $ignore) {
  212.                         // we get here if the item was deleted or moved locally. just ignore
  213.                     }
  214.                 }
  215.             }
  216.         }
  217.         /** @var $node NodeInterface */
  218.         $node $this->factory->get($class, [$object$absPath$this->session$this]);
  219.         if ($uuid $node->getIdentifier()) {
  220.             // Map even nodes that are not mix:referenceable, as long as they have a uuid
  221.             $this->objectsByUuid[$uuid] = $absPath;
  222.         }
  223.         $this->objectsByPath[$class][$absPath] = $node;
  224.         return $this->objectsByPath[$class][$absPath];
  225.     }
  226.     /**
  227.      * Get multiple nodes identified by an absolute paths. Missing nodes are ignored.
  228.      *
  229.      * Note paths that cannot be found will be ignored and missing from the result.
  230.      *
  231.      * Uses the factory to create Node objects.
  232.      *
  233.      * @param array $absPaths Array containing the absolute paths of the nodes to
  234.      *      fetch.
  235.      * @param string $class The class of node to get. TODO: Is it sane to
  236.      *      fetch data separately for Version and normal Node?
  237.      * @param array|null $typeFilter Node type list to skip some nodes
  238.      *
  239.      * @return Node[] Iterator that contains all found NodeInterface instances keyed by their path
  240.      *
  241.      * @throws RepositoryException If the path is not absolute or not well-formed
  242.      *
  243.      * @see Session::getNodes()
  244.      */
  245.     public function getNodesByPath($absPaths$class Node::class, $typeFilter null)
  246.     {
  247.         return new NodePathIterator($this$absPaths$class$typeFilter);
  248.     }
  249.     public function getNodesByPathAsArray($paths$class Node::class, $typeFilter null)
  250.     {
  251.         if (is_string($typeFilter)) {
  252.             $typeFilter = [$typeFilter];
  253.         }
  254.         $nodes $fetchPaths = [];
  255.         foreach ($paths as $absPath) {
  256.             if (!empty($this->objectsByPath[$class][$absPath])) {
  257.                 // Return it from memory if we already have it and type is correct
  258.                 if ($typeFilter
  259.                     && !$this->matchNodeType($this->objectsByPath[$class][$absPath], $typeFilter)
  260.                 ) {
  261.                     // skip this node if it did not match a type filter
  262.                     continue;
  263.                 }
  264.                 $nodes[$absPath] = $this->objectsByPath[$class][$absPath];
  265.             } else {
  266.                 $nodes[$absPath] = '';
  267.                 $fetchPaths[$absPath] = $this->getFetchPath($absPath$class);
  268.             }
  269.         }
  270.         $userlandTypeFilter false;
  271.         if (!empty($fetchPaths)) {
  272.             if ($typeFilter) {
  273.                 if ($this->transport instanceof NodeTypeFilterInterface) {
  274.                     $data $this->transport->getNodesFiltered($fetchPaths$typeFilter);
  275.                 } else {
  276.                     $data $this->transport->getNodes($fetchPaths);
  277.                     $userlandTypeFilter true;
  278.                 }
  279.             } else {
  280.                 $data $this->transport->getNodes($fetchPaths);
  281.             }
  282.             $inversePaths array_flip($fetchPaths);
  283.             foreach ($data as $fetchPath => $item) {
  284.                 // only add this node to the list if it was actually requested.
  285.                 if (isset($inversePaths[$fetchPath])) {
  286.                     // transform back to session paths from the fetch paths, in case of
  287.                     // a pending move operation
  288.                     $absPath $inversePaths[$fetchPath];
  289.                     $node $this->getNodeByPath($absPath$class$item);
  290.                     if ($userlandTypeFilter) {
  291.                         if (null !== $typeFilter && !$this->matchNodeType($node$typeFilter)) {
  292.                             continue;
  293.                         }
  294.                     }
  295.                     $nodes[$absPath] = $node;
  296.                     unset($inversePaths[$fetchPath]);
  297.                 } else {
  298.                     // this is either a prefetched node that was not requested
  299.                     // or it falls through the type filter. cache it.
  300.                     // first undo eventual move operation
  301.                     $parent $fetchPath;
  302.                     $relPath '';
  303.                     while ($parent) {
  304.                         if (isset($inversePaths[$parent])) {
  305.                             break;
  306.                         }
  307.                         if ('/' === $parent) {
  308.                             $parent false;
  309.                         } else {
  310.                             $parent PathHelper::getParentPath($parent);
  311.                             $relPath '/' PathHelper::getNodeName($parent) . $relPath;
  312.                         }
  313.                     }
  314.                     if ($parent) {
  315.                         $this->getNodeByPath($parent $relPath$class$item);
  316.                     }
  317.                 }
  318.             }
  319.             // clean away the not found paths from the final result
  320.             foreach ($inversePaths as $absPath) {
  321.                 unset($nodes[$absPath]);
  322.             }
  323.         }
  324.         return $nodes;
  325.     }
  326.     /**
  327.      * Check if a node is of any of the types listed in typeFilter.
  328.      *
  329.      * @param NodeInterface $node
  330.      * @param array         $typeFilter
  331.      *
  332.      * @return boolean
  333.      */
  334.     private function matchNodeType(NodeInterface $node, array $typeFilter)
  335.     {
  336.         foreach ($typeFilter as $type) {
  337.             if ($node->isNodeType($type)) {
  338.                 return true;
  339.             }
  340.         }
  341.         return false;
  342.     }
  343.     /**
  344.      * This method will either let the transport filter if that is possible or
  345.      * forward to getNodes and return the names of the nodes found there.,
  346.      *
  347.      * @param NodeInterface $node
  348.      * @param string|array  $nameFilter
  349.      * @param string|array  $typeFilter
  350.      *
  351.      * @return ArrayIterator
  352.      */
  353.     public function filterChildNodeNamesByType(NodeInterface $node$nameFilter$typeFilter)
  354.     {
  355.         if ($this->transport instanceof NodeTypeFilterInterface) {
  356.             return $this->transport->filterChildNodeNamesByType($node->getPath(), $node->getNodeNames($nameFilter), $typeFilter);
  357.         }
  358.         // fallback: get the actual nodes and let that filter. this is expensive.
  359.         return new ArrayIterator(array_keys($node->getNodes($nameFilter$typeFilter)->getArrayCopy()));
  360.     }
  361.     /**
  362.      * Resolve the path through all pending operations and sanity check while
  363.      * doing this.
  364.      *
  365.      * @param string $absPath The absolute path of the node to fetch.
  366.      * @param string $class   The class of node to get. TODO: Is it sane to fetch
  367.      *      data separately for Version and normal Node?
  368.      *
  369.      * @return string fetch path
  370.      *
  371.      * @throws ItemNotFoundException if while walking backwards through the
  372.      *      operations log we see this path was moved away or got deleted
  373.      * @throws RepositoryException
  374.      */
  375.     protected function getFetchPath($absPath$class)
  376.     {
  377.         $absPath PathHelper::normalizePath($absPath);
  378.         if (!isset($this->objectsByPath[$class])) {
  379.             $this->objectsByPath[$class] = [];
  380.         }
  381.         $op end($this->operationsLog);
  382.         while ($op) {
  383.             if ($op instanceof MoveNodeOperation) {
  384.                 if ($absPath === $op->srcPath) {
  385.                     throw new ItemNotFoundException("Path not found (moved in current session): $absPath");
  386.                 }
  387.                 if (strpos($absPath$op->srcPath '/') === 0) {
  388.                     throw new ItemNotFoundException("Path not found (parent node {$op->srcPath} moved in current session): $absPath");
  389.                 }
  390.                 if (strpos($absPath$op->dstPath '/') === || $absPath == $op->dstPath) {
  391.                     $absPathsubstr_replace($absPath$op->srcPath0strlen($op->dstPath));
  392.                 }
  393.             } elseif ($op instanceof RemoveNodeOperation || $op instanceof RemovePropertyOperation) {
  394.                 if ($absPath === $op->srcPath) {
  395.                     throw new ItemNotFoundException("Path not found (node deleted in current session): $absPath");
  396.                 }
  397.                 if (strpos($absPath$op->srcPath '/') === 0) {
  398.                     throw new ItemNotFoundException("Path not found (parent node {$op->srcPath} deleted in current session): $absPath");
  399.                 }
  400.             } elseif ($op instanceof AddNodeOperation) {
  401.                 if ($absPath === $op->srcPath) {
  402.                     // we added this node at this point so no more sanity checks needed.
  403.                     return $absPath;
  404.                 }
  405.             }
  406.             $op prev($this->operationsLog);
  407.         }
  408.         return $absPath;
  409.     }
  410.     /**
  411.      * Get the property identified by an absolute path.
  412.      *
  413.      * Uses the factory to instantiate a Property.
  414.      *
  415.      * Currently Jackalope just loads the containing node and then returns
  416.      * the requested property of the node instance.
  417.      *
  418.      * @param  string            $absPath The absolute path of the property to create.
  419.      * @return PropertyInterface
  420.      *
  421.      * @throws ItemNotFoundException if item is not found at this path
  422.      * @throws InvalidArgumentException
  423.      * @throws RepositoryException
  424.      */
  425.     public function getPropertyByPath($absPath)
  426.     {
  427.         list($name$nodep) = $this->getNodePath($absPath);
  428.         // OPTIMIZE: should use transport->getProperty - when we implement this, we must make sure only one instance of each property ever exists. and do the moved/deleted checks that are done in node
  429.         $n $this->getNodeByPath($nodep);
  430.         try {
  431.             return $n->getProperty($name); //throws PathNotFoundException if there is no such property
  432.         } catch (PathNotFoundException $e) {
  433.             throw new ItemNotFoundException($e->getMessage(), $e->getCode(), $e);
  434.         }
  435.     }
  436.     /**
  437.      * Get all nodes of those properties in one batch, then collect the
  438.      * properties of them.
  439.      *
  440.      * @param $absPaths
  441.      *
  442.      * @return ArrayIterator that contains all found PropertyInterface
  443.      *      instances keyed by their path
  444.      */
  445.     public function getPropertiesByPath($absPaths)
  446.     {
  447.         // list of nodes to fetch
  448.         $nodemap = [];
  449.         // ordered list of what to return
  450.         $returnmap = [];
  451.         foreach ($absPaths as $path) {
  452.             list($name$nodep) = $this->getNodePath($path);
  453.             if (! isset($nodemap[$nodep])) {
  454.                 $nodemap[$nodep] = $nodep;
  455.             }
  456.             $returnmap[$path] = ['name' => $name'path' => $nodep];
  457.         }
  458.         $nodes $this->getNodesByPath($nodemap);
  459.         $properties = [];
  460.         foreach ($returnmap as $key => $data) {
  461.             if (isset($nodes[$data['path']]) && $nodes[$data['path']]->hasProperty($data['name'])) {
  462.                 $properties[$key] = $nodes[$data['path']]->getProperty($data['name']);
  463.             }
  464.         }
  465.         return new ArrayIterator($properties);
  466.     }
  467.     /**
  468.      * Get the node path for a property, and the property name
  469.      *
  470.      * @param string $absPath
  471.      *
  472.      * @return array with name, node path
  473.      *
  474.      * @throws RepositoryException
  475.      */
  476.     protected function getNodePath($absPath)
  477.     {
  478.         $absPath PathHelper::normalizePath($absPath);
  479.         $name PathHelper::getNodeName($absPath); //the property name
  480.         $nodep PathHelper::getParentPath($absPath0strrpos($absPath'/') + 1); //the node this property should be in
  481.         return [$name$nodep];
  482.     }
  483.     /**
  484.      * Get the node identified by a relative path.
  485.      *
  486.      * If you have an absolute path use {@link getNodeByPath()} for better
  487.      * performance.
  488.      *
  489.      * @param string $relPath relative path
  490.      * @param string $context context path
  491.      * @param string $class   optional class name for the factory
  492.      *
  493.      * @return NodeInterface The specified Node. if not available,
  494.      *      ItemNotFoundException is thrown
  495.      *
  496.      * @throws ItemNotFoundException If the path was not found
  497.      * @throws RepositoryException   if another error occurs.
  498.      *
  499.      * @see Session::getNode()
  500.      */
  501.     public function getNode($relPath$context$class Node::class)
  502.     {
  503.         $path PathHelper::absolutizePath($relPath$context);
  504.         return $this->getNodeByPath($path$class);
  505.     }
  506.     /**
  507.      * Get the node identified by an uuid.
  508.      *
  509.      * @param string $identifier uuid
  510.      * @param string $class      optional class name for factory
  511.      *
  512.      * @return NodeInterface The specified Node. if not available,
  513.      *      ItemNotFoundException is thrown
  514.      *
  515.      * @throws ItemNotFoundException If the path was not found
  516.      * @throws RepositoryException   if another error occurs.
  517.      * @throws NoSuchWorkspaceException if the workspace was not found
  518.      *
  519.      * @see Session::getNodeByIdentifier()
  520.      */
  521.     public function getNodeByIdentifier($identifier$class Node::class)
  522.     {
  523.         if (empty($this->objectsByUuid[$identifier])) {
  524.             $data $this->transport->getNodeByIdentifier($identifier);
  525.             $path $data->{':jcr:path'};
  526.             unset($data->{':jcr:path'});
  527.             // TODO: $path is a backend path. we should inverse the getFetchPath operation here
  528.             $node $this->getNodeByPath($path$class$data);
  529.             $this->objectsByUuid[$identifier] = $path//only do this once the getNodeByPath has worked
  530.             return $node;
  531.         }
  532.         return $this->getNodeByPath($this->objectsByUuid[$identifier], $class);
  533.     }
  534.     /**
  535.      * Get the nodes identified by the given UUIDs.
  536.      *
  537.      * Note UUIDs that are not found will be ignored. Also, duplicate IDs
  538.      * will be eliminated by nature of using the IDs as keys.
  539.      *
  540.      * @param array  $identifiers UUIDs of nodes to retrieve.
  541.      * @param string $class       Optional class name for the factory.
  542.      *
  543.      * @return ArrayIterator|Node[] Iterator of the specified nodes keyed by their unique ids
  544.      *
  545.      * @throws RepositoryException if another error occurs.
  546.      *
  547.      * @see Session::getNodesByIdentifier()
  548.      */
  549.     public function getNodesByIdentifier($identifiers$class Node::class)
  550.     {
  551.         $nodes $fetchPaths = [];
  552.         foreach ($identifiers as $uuid) {
  553.             if (!empty($this->objectsByUuid[$uuid])
  554.                 && !empty($this->objectsByPath[$class][$this->objectsByUuid[$uuid]])
  555.             ) {
  556.                 // Return it from memory if we already have it
  557.                 $nodes[$uuid] = $this->objectsByPath[$class][$this->objectsByUuid[$uuid]];
  558.             } else {
  559.                 $fetchPaths[$uuid] = $uuid;
  560.                 $nodes[$uuid] = $uuid// keep position
  561.             }
  562.         }
  563.         if (!empty($fetchPaths)) {
  564.             $data $this->transport->getNodesByIdentifier($fetchPaths);
  565.             foreach ($data as $absPath => $item) {
  566.                 // TODO: $absPath is the backend path. we should inverse the getFetchPath operation here
  567.                 // build the node from the received data
  568.                 $node $this->getNodeByPath($absPath$class$item);
  569.                 $found[$node->getIdentifier()] = $node;
  570.             }
  571.             foreach ($nodes as $key => $node) {
  572.                 if (is_string($node)) {
  573.                     if (isset($found[$node])) {
  574.                         $nodes[$key] = $found[$node];
  575.                     } else {
  576.                         unset($nodes[$key]);
  577.                     }
  578.                 }
  579.             }
  580.         }
  581.         reset($nodes);
  582.         return new ArrayIterator($nodes);
  583.     }
  584.     /**
  585.      * Retrieves the stream for a binary value.
  586.      *
  587.      * @param string $path The absolute path to the stream
  588.      *
  589.      * @return resource
  590.      *
  591.      * @throws ItemNotFoundException
  592.      * @throws RepositoryException
  593.      */
  594.     public function getBinaryStream($path)
  595.     {
  596.         return $this->transport->getBinaryStream($this->getFetchPath($pathNode::class));
  597.     }
  598.     /**
  599.      * Returns the node types specified by name in the array or all types if no filter is given.
  600.      *
  601.      * This is only a proxy to the transport
  602.      *
  603.      * @param array $nodeTypes Empty for all or specify node types by name
  604.      *
  605.      * @return array|\DOMDocument containing the nodetype information
  606.      */
  607.     public function getNodeTypes(array $nodeTypes = [])
  608.     {
  609.         return $this->transport->getNodeTypes($nodeTypes);
  610.     }
  611.     /**
  612.      * Get a single nodetype.
  613.      *
  614.      * @param string $nodeType the name of nodetype to get from the transport
  615.      *
  616.      * @return \DOMDocument containing the nodetype information
  617.      *
  618.      * @see getNodeTypes()
  619.      */
  620.     public function getNodeType($nodeType)
  621.     {
  622.         return $this->getNodeTypes([$nodeType]);
  623.     }
  624.     /**
  625.      * Register node types with the backend.
  626.      *
  627.      * This is only a proxy to the transport
  628.      *
  629.      * @param array   $types       an array of NodeTypeDefinitions
  630.      * @param boolean $allowUpdate whether to fail if node already exists or to
  631.      *      update it
  632.      *
  633.      * @return bool true on success
  634.      *
  635.      * @throws InvalidNodeTypeDefinitionException
  636.      * @throws NodeTypeExistsException
  637.      * @throws RepositoryException
  638.      * @throws UnsupportedRepositoryOperationException
  639.      */
  640.     public function registerNodeTypes($types$allowUpdate)
  641.     {
  642.         if ($this->transport instanceof NodeTypeManagementInterface) {
  643.             return $this->transport->registerNodeTypes($types$allowUpdate);
  644.         }
  645.         if ($this->transport instanceof NodeTypeCndManagementInterface) {
  646.             $writer = new CndWriter($this->session->getWorkspace()->getNamespaceRegistry());
  647.             return $this->transport->registerNodeTypesCnd($writer->writeString($types), $allowUpdate);
  648.         }
  649.         throw new UnsupportedRepositoryOperationException('Transport does not support registering node types');
  650.     }
  651.     /**
  652.      * Returns all accessible REFERENCE properties in the workspace that point
  653.      * to the node
  654.      *
  655.      * @param string $path the path of the referenced node
  656.      * @param string $name name of referring REFERENCE properties to be
  657.      *      returned; if null then all referring REFERENCEs are returned
  658.      *
  659.      * @return ArrayIterator
  660.      *
  661.      * @see Node::getReferences()
  662.      */
  663.     public function getReferences($path$name null)
  664.     {
  665.         $references $this->transport->getReferences($this->getFetchPath($pathNode::class), $name);
  666.         return $this->pathArrayToPropertiesIterator($references);
  667.     }
  668.     /**
  669.      * Returns all accessible WEAKREFERENCE properties in the workspace that
  670.      * point to the node
  671.      *
  672.      * @param string $path the path of the referenced node
  673.      * @param string $name name of referring WEAKREFERENCE properties to be
  674.      *      returned; if null then all referring WEAKREFERENCEs are returned
  675.      *
  676.      * @return ArrayIterator
  677.      *
  678.      * @see Node::getWeakReferences()
  679.      */
  680.     public function getWeakReferences($path$name null)
  681.     {
  682.         $references $this->transport->getWeakReferences($this->getFetchPath($pathNode::class), $name);
  683.         return $this->pathArrayToPropertiesIterator($references);
  684.     }
  685.     /**
  686.      * Transform an array containing properties paths to an ArrayIterator over
  687.      * Property objects
  688.      *
  689.      * @param  array $propertyPaths an array of properties paths
  690.      *
  691.      * @return ArrayIterator
  692.      */
  693.     protected function pathArrayToPropertiesIterator($propertyPaths)
  694.     {
  695.         //FIXME: this will break if we have non-persisted move
  696.         return new ArrayIterator($this->getPropertiesByPath($propertyPaths));
  697.     }
  698.     /**
  699.      * Register node types with compact node definition format
  700.      *
  701.      * This is only a proxy to the transport
  702.      *
  703.      * @param  string  $cnd         a string with cnd information
  704.      * @param  boolean $allowUpdate whether to fail if node already exists or to update it
  705.      * @return bool|\Iterator    true on success or \Iterator over the registered node types if repository is not able to process
  706.      * CND directly
  707.      *
  708.      * @throws UnsupportedRepositoryOperationException
  709.      * @throws RepositoryException
  710.      * @throws AccessDeniedException
  711.      * @throws NamespaceException
  712.      * @throws InvalidNodeTypeDefinitionException
  713.      * @throws NodeTypeExistsException
  714.      *
  715.      * @see NodeTypeManagerInterface::registerNodeTypesCnd
  716.      */
  717.     public function registerNodeTypesCnd($cnd$allowUpdate)
  718.     {
  719.         if ($this->transport instanceof NodeTypeCndManagementInterface) {
  720.             return $this->transport->registerNodeTypesCnd($cnd$allowUpdate);
  721.         }
  722.         if ($this->transport instanceof NodeTypeManagementInterface) {
  723.             $workspace $this->session->getWorkspace();
  724.             $nsRegistry $workspace->getNamespaceRegistry();
  725.             $parser = new CndParser($workspace->getNodeTypeManager());
  726.             $res $parser->parseString($cnd);
  727.             $ns $res['namespaces'];
  728.             $types $res['nodeTypes'];
  729.             foreach ($ns as $prefix => $uri) {
  730.                 $nsRegistry->registerNamespace($prefix$uri);
  731.             }
  732.             return $workspace->getNodeTypeManager()->registerNodeTypes($types$allowUpdate);
  733.         }
  734.         throw new UnsupportedRepositoryOperationException('Transport does not support registering node types');
  735.     }
  736.     /**
  737.      * Push all recorded changes to the backend.
  738.      *
  739.      * The order is important to avoid conflicts
  740.      * 1. operationsLog
  741.      * 2. commit any other changes
  742.      *
  743.      * If transactions are enabled but we are not currently inside a
  744.      * transaction, the session is responsible to start a transaction to make
  745.      * sure the backend state does not get messed up in case of error.
  746.      */
  747.     public function save()
  748.     {
  749.         if (! $this->transport instanceof WritingInterface) {
  750.             throw new UnsupportedRepositoryOperationException('Transport does not support writing');
  751.         }
  752.         try {
  753.             $this->transport->prepareSave();
  754.             $this->executeOperations($this->operationsLog);
  755.             // loop through cached nodes and commit all dirty and set them to clean.
  756.             if (isset($this->objectsByPath[Node::class])) {
  757.                 foreach ($this->objectsByPath[Node::class] as $node) {
  758.                     /** @var $node Node */
  759.                     if ($node->isModified()) {
  760.                         if (! $node instanceof NodeInterface) {
  761.                             throw new RepositoryException('Internal Error: Unknown type '.get_class($node));
  762.                         }
  763.                         $this->transport->updateProperties($node);
  764.                         if ($node->needsChildReordering()) {
  765.                             $this->transport->reorderChildren($node);
  766.                         }
  767.                     }
  768.                 }
  769.             }
  770.             $this->transport->finishSave();
  771.         } catch (Exception $e) {
  772.             $this->transport->rollbackSave();
  773.             if (! $e instanceof RepositoryException) {
  774.                 throw new RepositoryException('Error inside the transport layer: '.$e->getMessage(), null$e);
  775.             }
  776.             throw $e;
  777.         }
  778.         foreach ($this->operationsLog as $operation) {
  779.             if ($operation instanceof MoveNodeOperation) {
  780.                 if (isset($this->objectsByPath[Node::class][$operation->dstPath])) {
  781.                     // might not be set if moved again afterwards
  782.                     // move is not treated as modified, need to confirm separately
  783.                     $this->objectsByPath[Node::class][$operation->dstPath]->confirmSaved();
  784.                 }
  785.             }
  786.         }
  787.         //clear those lists before reloading the newly added nodes from backend, to avoid collisions
  788.         $this->nodesRemove = [];
  789.         $this->propertiesRemove = [];
  790.         $this->nodesMove = [];
  791.         foreach ($this->operationsLog as $operation) {
  792.             if ($operation instanceof AddNodeOperation) {
  793.                 if (! $operation->node->isDeleted()) {
  794.                     $operation->node->confirmSaved();
  795.                 }
  796.             }
  797.         }
  798.         if (isset($this->objectsByPath[Node::class])) {
  799.             foreach ($this->objectsByPath[Node::class] as $item) {
  800.                 /** @var $item Item */
  801.                 if ($item->isModified() || $item->isMoved()) {
  802.                     $item->confirmSaved();
  803.                 }
  804.             }
  805.         }
  806.         $this->nodesAdd = [];
  807.         $this->operationsLog = [];
  808.     }
  809.     /**
  810.      * Execute the recorded operations in the right order, skipping
  811.      * stale data.
  812.      *
  813.      * @param Operation[] $operations
  814.      *
  815.      * @throws \Exception
  816.      */
  817.     protected function executeOperations(array $operations)
  818.     {
  819.         $lastType null;
  820.         $batch = [];
  821.         foreach ($operations as $operation) {
  822.             if ($operation->skip) {
  823.                 continue;
  824.             }
  825.             if (null === $lastType) {
  826.                 $lastType $operation->type;
  827.             }
  828.             if ($operation->type !== $lastType) {
  829.                 $this->executeBatch($lastType$batch);
  830.                 $lastType $operation->type;
  831.                 $batch = [];
  832.             }
  833.             $batch[] = $operation;
  834.         }
  835.         // only execute last batch if not all was skipped
  836.         if (! count($batch)) {
  837.             return;
  838.         }
  839.         $this->executeBatch($lastType$batch);
  840.     }
  841.     /**
  842.      * Execute a batch of operations of one type.
  843.      *
  844.      * @param int $type               type of the operations to be executed
  845.      * @param Operation[] $operations list of same type operations
  846.      *
  847.      * @throws \Exception
  848.      */
  849.     protected function executeBatch($type$operations)
  850.     {
  851.         switch ($type) {
  852.             case Operation::ADD_NODE:
  853.                 $this->transport->storeNodes($operations);
  854.                 break;
  855.             case Operation::MOVE_NODE:
  856.                 $this->transport->moveNodes($operations);
  857.                 break;
  858.             case Operation::REMOVE_NODE:
  859.                 $this->transport->deleteNodes($operations);
  860.                 break;
  861.             case Operation::REMOVE_PROPERTY:
  862.                 $this->transport->deleteProperties($operations);
  863.                 break;
  864.             default:
  865.                 throw new Exception("internal error: unknown operation '$type'");
  866.         }
  867.     }
  868.     /**
  869.      * Removes the cache of the predecessor version after the node has been checked in.
  870.      *
  871.      * TODO: document more clearly
  872.      *
  873.      * @see VersionManager::checkin
  874.      *
  875.      * @param string $absPath
  876.      * @return VersionInterface node version
  877.      *
  878.      * @throws ItemNotFoundException
  879.      * @throws RepositoryException
  880.      */
  881.     public function checkin($absPath)
  882.     {
  883.         $path $this->transport->checkinItem($absPath); //FIXME: what about pending move operations?
  884.         return $this->getNodeByPath($pathVersion::class);
  885.     }
  886.     /**
  887.      * Removes the cache of the predecessor version after the node has been checked in.
  888.      *
  889.      * TODO: document more clearly. This looks like copy-paste from checkin
  890.      *
  891.      * @see VersionManager::checkout
  892.      */
  893.     public function checkout($absPath)
  894.     {
  895.         $this->transport->checkoutItem($absPath); //FIXME: what about pending move operations?
  896.     }
  897.     /**
  898.      * @see VersioningInterface::addVersionLabel
  899.      */
  900.     public function addVersionLabel($path$label$moveLabel)
  901.     {
  902.         $this->transport->addVersionLabel($path$label$moveLabel);
  903.     }
  904.     /**
  905.      * @see VersioningInterface::addVersionLabel
  906.      */
  907.     public function removeVersionLabel($path$label)
  908.     {
  909.         $this->transport->removeVersionLabel($path$label);
  910.     }
  911.     /**
  912.      * Restore the node at $nodePath to the version at $versionPath
  913.      *
  914.      * Clears the node's cache after it has been restored.
  915.      *
  916.      * TODO: This is incomplete. Needs batch processing to implement restoring an array of versions
  917.      *
  918.      * @param bool $removeExisting whether to remove the existing current
  919.      *      version or create a new version after that version
  920.      * @param string $versionPath
  921.      * @param string $nodePath    absolute path to the node
  922.      */
  923.     public function restore($removeExisting$versionPath$nodePath)
  924.     {
  925.         // TODO: handle pending move operations?
  926.         if (isset($this->objectsByPath[Node::class][$nodePath])) {
  927.             $this->objectsByPath[Node::class][$nodePath]->setChildrenDirty();
  928.             $this->objectsByPath[Node::class][$nodePath]->setDirty();
  929.         }
  930.         if (isset($this->objectsByPath[Version::class][$versionPath])) {
  931.             $this->objectsByPath[Version::class][$versionPath]->setChildrenDirty();
  932.             $this->objectsByPath[Version::class][$versionPath]->setDirty();
  933.         }
  934.         $this->transport->restoreItem($removeExisting$versionPath$nodePath);
  935.     }
  936.     /**
  937.      * Remove a version given the path to the version node and the version name.
  938.      *
  939.      * @param string $versionPath The path to the version node
  940.      * @param string $versionName The name of the version to remove
  941.      *
  942.      * @throws UnsupportedRepositoryOperationException
  943.      * @throws ReferentialIntegrityException
  944.      * @throws VersionException
  945.      */
  946.     public function removeVersion($versionPath$versionName)
  947.     {
  948.         $this->transport->removeVersion($versionPath$versionName);
  949.         // Adjust the in memory state
  950.         $absPath $versionPath '/' $versionName;
  951.         if (isset($this->objectsByPath[Node::class][$absPath])) {
  952.             /** @var $node Node */
  953.             $node $this->objectsByPath[Node::class][$absPath];
  954.             unset($this->objectsByUuid[$node->getIdentifier()]);
  955.             $node->setDeleted();
  956.         }
  957.         if (isset($this->objectsByPath[Version::class][$absPath])) {
  958.             /** @var $version Version */
  959.             $version $this->objectsByPath[Version::class][$absPath];
  960.             unset($this->objectsByUuid[$version->getIdentifier()]);
  961.             $version->setDeleted();
  962.         }
  963.         unset(
  964.             $this->objectsByPath[Node::class][$absPath],
  965.             $this->objectsByPath[Version::class][$absPath]
  966.         );
  967.         $this->cascadeDelete($absPathfalse);
  968.         $this->cascadeDeleteVersion($absPath);
  969.     }
  970.     /**
  971.      * Refresh cached items from the backend.
  972.      *
  973.      * @param boolean $keepChanges whether to keep local changes or discard
  974.      *      them.
  975.      *
  976.      * @see Session::refresh()
  977.      */
  978.     public function refresh($keepChanges)
  979.     {
  980.         if (! $keepChanges) {
  981.             // revert all scheduled add, remove and move operations
  982.             $this->operationsLog = [];
  983.             foreach ($this->nodesAdd as $path => $operation) {
  984.                 if (! $operation->skip) {
  985.                     $operation->node->setDeleted();
  986.                     unset($this->objectsByPath[Node::class][$path]); // did you see anything? it never existed
  987.                 }
  988.             }
  989.             $this->nodesAdd = [];
  990.             // the code below will set this to dirty again. but it must not
  991.             // be in state deleted or we will fail the sanity checks
  992.             foreach ($this->propertiesRemove as $path => $operation) {
  993.                 $operation->property->setClean();
  994.             }
  995.             $this->propertiesRemove = [];
  996.             foreach ($this->nodesRemove as $path => $operation) {
  997.                 $operation->node->setClean();
  998.                 $this->objectsByPath[Node::class][$path] = $operation->node// back in glory
  999.                 $parentPath PathHelper::getParentPath($path);
  1000.                 if (array_key_exists($parentPath$this->objectsByPath[Node::class])) {
  1001.                     // tell the parent about its restored child
  1002.                     $this->objectsByPath[Node::class][$parentPath]->addChildNode($operation->nodefalse);
  1003.                 }
  1004.             }
  1005.             $this->nodesRemove = [];
  1006.             foreach (array_reverse($this->nodesMove) as $operation) {
  1007.                 if (isset($this->objectsByPath[Node::class][$operation->dstPath])) {
  1008.                     // not set if we moved twice
  1009.                     $item $this->objectsByPath[Node::class][$operation->dstPath];
  1010.                     $item->setPath($operation->srcPath);
  1011.                 }
  1012.                 $parentPath PathHelper::getParentPath($operation->dstPath);
  1013.                 if (array_key_exists($parentPath$this->objectsByPath[Node::class])) {
  1014.                     // tell the parent about its restored child
  1015.                     $this->objectsByPath[Node::class][$parentPath]->unsetChildNode(PathHelper::getNodeName($operation->dstPath), false);
  1016.                 }
  1017.                 // TODO: from in a two step move might fail. we should merge consecutive moves
  1018.                 $parentPath PathHelper::getParentPath($operation->srcPath);
  1019.                 if (array_key_exists($parentPath$this->objectsByPath[Node::class]) && isset($item) && $item instanceof Node) {
  1020.                     // tell the parent about its restored child
  1021.                     $this->objectsByPath[Node::class][$parentPath]->addChildNode($itemfalse);
  1022.                 }
  1023.                 // move item to old location
  1024.                 $this->objectsByPath[Node::class][$operation->srcPath] = $this->objectsByPath[Node::class][$operation->dstPath];
  1025.                 unset($this->objectsByPath[Node::class][$operation->dstPath]);
  1026.             }
  1027.             $this->nodesMove = [];
  1028.         }
  1029.         $this->objectsByUuid = [];
  1030.         /** @var $node Node */
  1031.         foreach ($this->objectsByPath[Node::class] as $node) {
  1032.             if (! $keepChanges || ! ($node->isDeleted() || $node->isNew())) {
  1033.                 // if we keep changes, do not restore a deleted item
  1034.                 $this->objectsByUuid[$node->getIdentifier()] = $node->getPath();
  1035.                 $node->setDirty($keepChanges);
  1036.             }
  1037.         }
  1038.     }
  1039.     /**
  1040.      * Determine if any object is modified and not saved to storage.
  1041.      *
  1042.      * @return boolean true if this session has any pending changes.
  1043.      *
  1044.      * @see Session::hasPendingChanges()
  1045.      */
  1046.     public function hasPendingChanges()
  1047.     {
  1048.         if (count($this->operationsLog)) {
  1049.             return true;
  1050.         }
  1051.         foreach ($this->objectsByPath[Node::class] as $item) {
  1052.             if ($item->isModified()) {
  1053.                 return true;
  1054.             }
  1055.         }
  1056.         return false;
  1057.     }
  1058.     /**
  1059.      * Remove the item at absPath from local cache and keep information for undo.
  1060.      *
  1061.      * @param string $absPath The absolute path of the item that is being
  1062.      *      removed. Note that contrary to removeItem(), this path is the full
  1063.      *      path for a property too.
  1064.      * @param PropertyInterface $property         The item that is being removed
  1065.      * @param bool              $sessionOperation whether the property removal should be
  1066.      *      dispatched immediately or needs to be scheduled in the operations log
  1067.      *
  1068.      * @see ObjectManager::removeItem()
  1069.      */
  1070.     protected function performPropertyRemove($absPathPropertyInterface $property$sessionOperation true)
  1071.     {
  1072.         if ($sessionOperation) {
  1073.             if ($property->isNew()) {
  1074.                 return;
  1075.             }
  1076.             // keep reference to object in case of refresh
  1077.             $operation = new RemovePropertyOperation($absPath$property);
  1078.             $this->propertiesRemove[$absPath] = $operation;
  1079.             $this->operationsLog[] = $operation;
  1080.             return;
  1081.         }
  1082.         // this is no session operation
  1083.         $this->transport->deletePropertyImmediately($absPath);
  1084.     }
  1085.     /**
  1086.      * Remove the item at absPath from local cache and keep information for undo.
  1087.      *
  1088.      * @param string $absPath The absolute path of the item that is being
  1089.      *      removed. Note that contrary to removeItem(), this path is the full
  1090.      *      path for a property too.
  1091.      * @param NodeInterface $node             The item that is being removed
  1092.      * @param bool          $sessionOperation whether the node removal should be
  1093.      *      dispatched immediately or needs to be scheduled in the operations log
  1094.      *
  1095.      * @see ObjectManager::removeItem()
  1096.      */
  1097.     protected function performNodeRemove($absPathNodeInterface $node$sessionOperation true$cascading false)
  1098.     {
  1099.         if (! $sessionOperation && ! $cascading) {
  1100.             $this->transport->deleteNodeImmediately($absPath);
  1101.         }
  1102.         unset(
  1103.             $this->objectsByUuid[$node->getIdentifier()],
  1104.             $this->objectsByPath[Node::class][$absPath]
  1105.         );
  1106.         if ($sessionOperation) {
  1107.             // keep reference to object in case of refresh
  1108.             $operation = new RemoveNodeOperation($absPath$node);
  1109.             $this->nodesRemove[$absPath] = $operation;
  1110.             if (! $cascading) {
  1111.                 $this->operationsLog[] = $operation;
  1112.             }
  1113.         }
  1114.     }
  1115.     /**
  1116.      * Notify all cached children that they are deleted as well and clean up
  1117.      * internal state
  1118.      *
  1119.      * @param string $absPath          parent node that was removed
  1120.      * @param bool   $sessionOperation to carry over the session operation information
  1121.      */
  1122.     protected function cascadeDelete($absPath$sessionOperation true)
  1123.     {
  1124.         foreach ($this->objectsByPath[Node::class] as $path => $node) {
  1125.             if (strpos($path"$absPath/") === 0) {
  1126.                 // notify item and let it call removeItem again. save()
  1127.                 // makes sure no children of already deleted items are
  1128.                 // deleted again.
  1129.                 $this->performNodeRemove($path$node$sessionOperationtrue);
  1130.                 if (!$node->isDeleted()) {
  1131.                     $node->setDeleted();
  1132.                 }
  1133.             }
  1134.         }
  1135.     }
  1136.     /**
  1137.      * Notify all cached version children that they are deleted as well and clean up
  1138.      * internal state
  1139.      *
  1140.      * @param string $absPath parent version node that was removed
  1141.      */
  1142.     protected function cascadeDeleteVersion($absPath)
  1143.     {
  1144.         // delete all versions, similar to cascadeDelete
  1145.         foreach ($this->objectsByPath[Version::class] as $path => $node) {
  1146.             if (strpos($path"$absPath/") === 0) {
  1147.                 // versions are read only, we simple unset them
  1148.                 unset(
  1149.                     $this->objectsByUuid[$node->getIdentifier()],
  1150.                     $this->objectsByPath[Version::class][$absPath]
  1151.                 );
  1152.                 if (!$node->isDeleted()) {
  1153.                     $node->setDeleted();
  1154.                 }
  1155.             }
  1156.         }
  1157.     }
  1158.     /**
  1159.      * Remove a node or a property.
  1160.      *
  1161.      * If this is a node, sets all cached items below this node to deleted as
  1162.      * well.
  1163.      *
  1164.      * If property is set, the path denotes the node containing the property,
  1165.      * otherwise the node at path is removed.
  1166.      *
  1167.      * @param string $absPath The absolute path to the node to be removed,
  1168.      *      including the node name.
  1169.      * @param PropertyInterface $property optional, property instance to delete from the
  1170.      *      given node path. If set, absPath is the path to the node containing
  1171.      *      this property.
  1172.      *
  1173.      * @throws RepositoryException If node cannot be found at given path
  1174.      *
  1175.      * @see Item::remove()
  1176.      */
  1177.     public function removeItem($absPathPropertyInterface $property null)
  1178.     {
  1179.         if (! $this->transport instanceof WritingInterface) {
  1180.             throw new UnsupportedRepositoryOperationException('Transport does not support writing');
  1181.         }
  1182.         // the object is always cached as invocation flow goes through Item::remove() without exception
  1183.         if (!isset($this->objectsByPath[Node::class][$absPath])) {
  1184.             throw new RepositoryException("Internal error: Item not found in local cache at $absPath");
  1185.         }
  1186.         if ($property) {
  1187.             $absPath PathHelper::absolutizePath($property->getName(), $absPath);
  1188.             $this->performPropertyRemove($absPath$property);
  1189.         } else {
  1190.             $node $this->objectsByPath[Node::class][$absPath];
  1191.             $this->performNodeRemove($absPath$node);
  1192.             $this->cascadeDelete($absPath);
  1193.         }
  1194.     }
  1195.     /**
  1196.      * Rewrites the path of a node for the movement operation, also updating
  1197.      * all cached children.
  1198.      *
  1199.      * This applies both to the cache and to the items themselves so
  1200.      * they return the correct value on getPath calls.
  1201.      *
  1202.      * @param string  $curPath Absolute path of the node to rewrite
  1203.      * @param string  $newPath The new absolute path
  1204.      */
  1205.     protected function rewriteItemPaths($curPath$newPath)
  1206.     {
  1207.         // update internal references in parent
  1208.         $parentCurPath PathHelper::getParentPath($curPath);
  1209.         $parentNewPath PathHelper::getParentPath($newPath);
  1210.         if (isset($this->objectsByPath[Node::class][$parentCurPath])) {
  1211.             /** @var $node Node */
  1212.             $node $this->objectsByPath[Node::class][$parentCurPath];
  1213.             if (! $node->hasNode(PathHelper::getNodeName($curPath))) {
  1214.                 throw new PathNotFoundException("Source path can not be found: $curPath");
  1215.             }
  1216.             $node->unsetChildNode(PathHelper::getNodeName($curPath), true);
  1217.         }
  1218.         if (isset($this->objectsByPath[Node::class][$parentNewPath])) {
  1219.             /** @var $node Node */
  1220.             $node $this->objectsByPath[Node::class][$parentNewPath];
  1221.             $node->addChildNode($this->getNodeByPath($curPath), truePathHelper::getNodeName($newPath));
  1222.         }
  1223.         // propagate to current and children items of $curPath, updating internal path
  1224.         /** @var $node Node */
  1225.         foreach ($this->objectsByPath[Node::class] as $path => $node) {
  1226.             // is it current or child?
  1227.             if ((strpos($path$curPath '/') === 0)||($path == $curPath)) {
  1228.                 // curPath = /foo
  1229.                 // newPath = /mo
  1230.                 // path    = /foo/bar
  1231.                 // newItemPath= /mo/bar
  1232.                 $newItemPath substr_replace($path$newPath0strlen($curPath));
  1233.                 if (isset($this->objectsByPath[Node::class][$path])) {
  1234.                     $node $this->objectsByPath[Node::class][$path];
  1235.                     $this->objectsByPath[Node::class][$newItemPath] = $node;
  1236.                     unset($this->objectsByPath[Node::class][$path]);
  1237.                     $node->setPath($newItemPathtrue);
  1238.                 }
  1239.                 // update uuid cache
  1240.                 $this->objectsByUuid[$node->getIdentifier()] = $node->getPath();
  1241.             }
  1242.         }
  1243.     }
  1244.     /**
  1245.      * WRITE: move node from source path to destination path
  1246.      *
  1247.      * @param string $srcAbsPath  Absolute path to the source node.
  1248.      * @param string $destAbsPath Absolute path to the destination where the node shall be moved to.
  1249.      *
  1250.      * @throws RepositoryException If node cannot be found at given path
  1251.      *
  1252.      * @see Session::move()
  1253.      */
  1254.     public function moveNode($srcAbsPath$destAbsPath)
  1255.     {
  1256.         if (! $this->transport instanceof WritingInterface) {
  1257.             throw new UnsupportedRepositoryOperationException('Transport does not support writing');
  1258.         }
  1259.         $srcAbsPath PathHelper::normalizePath($srcAbsPath);
  1260.         $destAbsPath PathHelper::normalizePath($destAbsPathtrue);
  1261.         $this->rewriteItemPaths($srcAbsPath$destAbsPathtrue);
  1262.         // record every single move in case we have intermediary operations
  1263.         $operation = new MoveNodeOperation($srcAbsPath$destAbsPath);
  1264.         $this->operationsLog[] = $operation;
  1265.         // update local cache state information
  1266.         if ($original $this->getMoveSrcPath($srcAbsPath)) {
  1267.             $srcAbsPath $original;
  1268.         }
  1269.         $this->nodesMove[$srcAbsPath] = $operation;
  1270.     }
  1271.     /**
  1272.      * Implement the workspace move method. It is dispatched to transport
  1273.      * immediately.
  1274.      *
  1275.      * @param string $srcAbsPath  the path of the node to be moved.
  1276.      * @param string $destAbsPath the location to which the node at srcAbsPath
  1277.      *      is to be moved.
  1278.      *
  1279.      * @throws RepositoryException If node cannot be found at given path
  1280.      *
  1281.      * @see Workspace::move()
  1282.      */
  1283.     public function moveNodeImmediately($srcAbsPath$destAbsPath)
  1284.     {
  1285.         if (! $this->transport instanceof WritingInterface) {
  1286.             throw new UnsupportedRepositoryOperationException('Transport does not support writing');
  1287.         }
  1288.         $srcAbsPath PathHelper::normalizePath($srcAbsPath);
  1289.         $destAbsPath PathHelper::normalizePath($destAbsPathtrue);
  1290.         $this->transport->moveNodeImmediately($srcAbsPath$destAbsPathtrue); // should throw the right exceptions
  1291.         $this->rewriteItemPaths($srcAbsPath$destAbsPath); // update local cache
  1292.     }
  1293.     /**
  1294.      * Implement the workspace removeItem method.
  1295.      *
  1296.      * @param string $absPath the absolute path of the item to be removed
  1297.      *
  1298.      * @see Workspace::removeItem
  1299.      */
  1300.     public function removeItemImmediately($absPath)
  1301.     {
  1302.         if (! $this->transport instanceof WritingInterface) {
  1303.             throw new UnsupportedRepositoryOperationException('Transport does not support writing');
  1304.         }
  1305.         $absPath PathHelper::normalizePath($absPath);
  1306.         $item $this->session->getItem($absPath);
  1307.         // update local state and cached objects about disappeared nodes
  1308.         if ($item instanceof NodeInterface) {
  1309.             $this->performNodeRemove($absPath$itemfalse);
  1310.             $this->cascadeDelete($absPathfalse);
  1311.         } else {
  1312.             $this->performPropertyRemove($absPath$itemfalse);
  1313.         }
  1314.         $item->setDeleted();
  1315.     }
  1316.     /**
  1317.      * Implement the workspace copy method. It is dispatched immediately.
  1318.      *
  1319.      * @param string $srcAbsPath  the path of the node to be copied.
  1320.      * @param string $destAbsPath the location to which the node at srcAbsPath
  1321.      *      is to be copied in this workspace.
  1322.      * @param string $srcWorkspace the name of the workspace from which the
  1323.      *      copy is to be made.
  1324.      *
  1325.      * @throws UnsupportedRepositoryOperationException
  1326.      * @throws RepositoryException
  1327.      * @throws ItemExistsException
  1328.      *
  1329.      * @see Workspace::copy()
  1330.      */
  1331.     public function copyNodeImmediately($srcAbsPath$destAbsPath$srcWorkspace)
  1332.     {
  1333.         if (! $this->transport instanceof WritingInterface) {
  1334.             throw new UnsupportedRepositoryOperationException('Transport does not support writing');
  1335.         }
  1336.         $srcAbsPath PathHelper::normalizePath($srcAbsPath);
  1337.         $destAbsPath PathHelper::normalizePath($destAbsPathtrue);
  1338.         if ($this->session->nodeExists($destAbsPath)) {
  1339.             throw new ItemExistsException('Node already exists at destination (update-on-copy is currently not supported)');
  1340.             // to support this, we would have to update the local cache of nodes as well
  1341.         }
  1342.         $this->transport->copyNode($srcAbsPath$destAbsPath$srcWorkspace);
  1343.     }
  1344.     /**
  1345.      * Implement the workspace clone method. It is dispatched immediately.
  1346.      *      http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.10%20Corresponding%20Nodes
  1347.      *      http://www.day.com/specs/jcr/2.0/10_Writing.html#10.8%20Cloning%20and%20Updating%20Nodes
  1348.      *
  1349.      * @param string  $srcWorkspace   the name of the workspace from which the copy is to be made.
  1350.      * @param string  $srcAbsPath     the path of the node to be cloned.
  1351.      * @param string  $destAbsPath    the location to which the node at srcAbsPath is to be cloned in this workspace.
  1352.      * @param boolean $removeExisting
  1353.      *
  1354.      * @throws UnsupportedRepositoryOperationException
  1355.      * @throws RepositoryException
  1356.      * @throws ItemExistsException
  1357.      *
  1358.      * @see Workspace::cloneFrom()
  1359.      */
  1360.     public function cloneFromImmediately($srcWorkspace$srcAbsPath$destAbsPath$removeExisting)
  1361.     {
  1362.         if (! $this->transport instanceof WritingInterface) {
  1363.             throw new UnsupportedRepositoryOperationException('Transport does not support writing');
  1364.         }
  1365.         $srcAbsPath PathHelper::normalizePath($srcAbsPath);
  1366.         $destAbsPath PathHelper::normalizePath($destAbsPathtrue);
  1367.         if (! $removeExisting && $this->session->nodeExists($destAbsPath)) {
  1368.             throw new ItemExistsException('Node already exists at destination and removeExisting is false');
  1369.         }
  1370.         $this->transport->cloneFrom($srcWorkspace$srcAbsPath$destAbsPath$removeExisting);
  1371.     }
  1372.     /**
  1373.      * WRITE: add a node at the specified path. Schedules an add operation
  1374.      * for the next save() and caches the node.
  1375.      *
  1376.      * @param string        $absPath the path to the node or property, including the item name
  1377.      * @param NodeInterface $node    The item instance that is added.
  1378.      *
  1379.      * @throws UnsupportedRepositoryOperationException
  1380.      * @throws ItemExistsException if a node already exists at that path
  1381.      */
  1382.     public function addNode($absPathNodeInterface $node)
  1383.     {
  1384.         if (! $this->transport instanceof WritingInterface) {
  1385.             throw new UnsupportedRepositoryOperationException('Transport does not support writing');
  1386.         }
  1387.         if (isset($this->objectsByPath[Node::class][$absPath])) {
  1388.             throw new ItemExistsException($absPath); //FIXME: same-name-siblings...
  1389.         }
  1390.         $this->objectsByPath[Node::class][$absPath] = $node;
  1391.         // a new item never has a uuid, no need to add to objectsByUuid
  1392.         $operation = new AddNodeOperation($absPath$node);
  1393.         $this->nodesAdd[$absPath] = $operation;
  1394.         $this->operationsLog[] = $operation;
  1395.     }
  1396.     /**
  1397.      * Return the permissions of the current session on the node given by path.
  1398.      * Permission can be of 4 types:
  1399.      *
  1400.      * - add_node
  1401.      * - read
  1402.      * - remove
  1403.      * - set_property
  1404.      *
  1405.      * This function will return an array containing zero, one or more of the
  1406.      * above strings.
  1407.      *
  1408.      * @param string $absPath absolute path to node to get permissions for it
  1409.      *
  1410.      * @return array of string
  1411.      *
  1412.      * @throws UnsupportedRepositoryOperationException
  1413.      */
  1414.     public function getPermissions($absPath)
  1415.     {
  1416.         if (! $this->transport instanceof PermissionInterface) {
  1417.             throw new UnsupportedRepositoryOperationException('Transport does not support permissions');
  1418.         }
  1419.         return $this->transport->getPermissions($absPath);
  1420.     }
  1421.     /**
  1422.      * Clears the state of the current session
  1423.      *
  1424.      * Removes all cached objects, planned changes etc. Mostly useful for
  1425.      * testing purposes.
  1426.      *
  1427.      * @deprecated: this will screw up major, as the user of the api can still have references to nodes. USE refresh instead!
  1428.      */
  1429.     public function clear()
  1430.     {
  1431.         $this->objectsByPath = [Node::class => []];
  1432.         $this->objectsByUuid = [];
  1433.         $this->nodesAdd = [];
  1434.         $this->nodesRemove = [];
  1435.         $this->propertiesRemove = [];
  1436.         $this->nodesMove = [];
  1437.     }
  1438.     /**
  1439.      * Implementation specific: Transport is used elsewhere, provide it here
  1440.      * for Session
  1441.      *
  1442.      * @return TransportInterface
  1443.      */
  1444.     public function getTransport()
  1445.     {
  1446.         return $this->transport;
  1447.     }
  1448.     /**
  1449.      * Begin new transaction associated with current session.
  1450.      *
  1451.      * @throws RepositoryException if the transaction implementation
  1452.      *      encounters an unexpected error condition.
  1453.      * @throws |InvalidArgumentException
  1454.      */
  1455.     public function beginTransaction()
  1456.     {
  1457.         $this->notifyItems('beginTransaction');
  1458.         $this->transport->beginTransaction();
  1459.     }
  1460.     /**
  1461.      * Complete the transaction associated with the current session.
  1462.      *
  1463.      * TODO: Make sure RollbackException and AccessDeniedException are thrown
  1464.      * by the transport if corresponding problems occur.
  1465.      *
  1466.      * @throws RollbackException if the transaction failed
  1467.      *      and was rolled back rather than committed.
  1468.      * @throws AccessDeniedException if the session is not allowed to
  1469.      *      commit the transaction.
  1470.      * @throws RepositoryException if the transaction implementation
  1471.      *      encounters an unexpected error condition.
  1472.      */
  1473.     public function commitTransaction()
  1474.     {
  1475.         $this->notifyItems('commitTransaction');
  1476.         $this->transport->commitTransaction();
  1477.     }
  1478.     /**
  1479.      * Roll back the transaction associated with the current session.
  1480.      *
  1481.      * TODO: Make sure AccessDeniedException is thrown by the transport
  1482.      * if corresponding problems occur
  1483.      * TODO: restore the in-memory state as it would be if save() was never
  1484.      * called during the transaction. The save() method will need to track some
  1485.      * undo information for this to be possible.
  1486.      *
  1487.      * @throws AccessDeniedException if the session is not allowed to
  1488.      *      roll back the transaction.
  1489.      * @throws RepositoryException if the transaction implementation
  1490.      *      encounters an unexpected error condition.
  1491.      */
  1492.     public function rollbackTransaction()
  1493.     {
  1494.         $this->transport->rollbackTransaction();
  1495.         $this->notifyItems('rollbackTransaction');
  1496.     }
  1497.     /**
  1498.      * Notifies the given node and all of its children and properties that a
  1499.      * transaction has begun, was committed or rolled back so that the item has
  1500.      * a chance to save or restore his internal state.
  1501.      *
  1502.      * @param string $method The method to call on each item for the
  1503.      *      notification (must be beginTransaction, commitTransaction or
  1504.      *      rollbackTransaction)
  1505.      *
  1506.      * @throws InvalidArgumentException if the passed $method is not valid
  1507.      */
  1508.     protected function notifyItems($method)
  1509.     {
  1510.         if (! in_array($method, ['beginTransaction''commitTransaction''rollbackTransaction'])) {
  1511.             throw new InvalidArgumentException("Unknown notification method '$method'");
  1512.         }
  1513.         // Notify the loaded nodes
  1514.         foreach ($this->objectsByPath[Node::class] as $node) {
  1515.             $node->$method();
  1516.         }
  1517.         // Notify the deleted nodes
  1518.         foreach ($this->nodesRemove as $op) {
  1519.             $op->node->$method();
  1520.         }
  1521.         // Notify the deleted properties
  1522.         foreach ($this->propertiesRemove as $op) {
  1523.             $op->property->$method();
  1524.         }
  1525.     }
  1526.     /**
  1527.      * Check whether a node path has an unpersisted move operation.
  1528.      *
  1529.      * This is a simplistic check to be used by the Node to determine if it
  1530.      * should not show one of the children the backend told it would exist.
  1531.      *
  1532.      * @param string $absPath The absolute path of the node
  1533.      *
  1534.      * @return boolean true if the node has an unsaved move operation, false
  1535.      *      otherwise
  1536.      *
  1537.      * @see Node::__construct
  1538.      */
  1539.     public function isNodeMoved($absPath)
  1540.     {
  1541.         return array_key_exists($absPath$this->nodesMove);
  1542.     }
  1543.     /**
  1544.      * Get the src path of a move operation knowing the target path.
  1545.      *
  1546.      * @param string $dstPath
  1547.      *
  1548.      * @return string|bool the source path if found, false otherwise
  1549.      */
  1550.     private function getMoveSrcPath($dstPath)
  1551.     {
  1552.         foreach ($this->nodesMove as $operation) {
  1553.             if ($operation->dstPath === $dstPath) {
  1554.                 return $operation->srcPath;
  1555.             }
  1556.         }
  1557.         return false;
  1558.     }
  1559.     /**
  1560.      * Check whether the node at path has an unpersisted delete operation and
  1561.      * there is no other node moved or added there.
  1562.      *
  1563.      * This is a simplistic check to be used by the Node to determine if it
  1564.      * should not show one of the children the backend told it would exist.
  1565.      *
  1566.      * @param string $absPath The absolute path of the node
  1567.      *
  1568.      * @return boolean true if the current changed state has no node at this place
  1569.      *
  1570.      * @see Node::__construct
  1571.      */
  1572.     public function isNodeDeleted($absPath)
  1573.     {
  1574.         return array_key_exists($absPath$this->nodesRemove)
  1575.             && !(array_key_exists($absPath$this->nodesAdd) && !$this->nodesAdd[$absPath]->skip
  1576.                 || $this->getMoveSrcPath($absPath));
  1577.     }
  1578.     /**
  1579.      * Get a node if it is already in cache or null otherwise.
  1580.      *
  1581.      * Note that this method will also return deleted node objects so you can
  1582.      * use them in refresh operations.
  1583.      *
  1584.      * @param string $absPath the absolute path to the node to fetch from cache
  1585.      *
  1586.      * @return NodeInterface or null
  1587.      *
  1588.      * @see Node::refresh()
  1589.      */
  1590.     public function getCachedNode($absPath$class Node::class)
  1591.     {
  1592.         if (isset($this->objectsByPath[$class][$absPath])) {
  1593.             return $this->objectsByPath[$class][$absPath];
  1594.         }
  1595.         if (array_key_exists($absPath$this->nodesRemove)) {
  1596.             return $this->nodesRemove[$absPath]->node;
  1597.         }
  1598.         return null;
  1599.     }
  1600.     /**
  1601.      * Return an ArrayIterator containing all the cached children of the given node.
  1602.      * It makes no difference whether or not the node itself is cached.
  1603.      *
  1604.      * Note that this method will also return deleted node objects so you can
  1605.      * use them in refresh operations.
  1606.      *
  1607.      * @param string $absPath
  1608.      * @param string $class
  1609.      *
  1610.      * @return ArrayIterator
  1611.      */
  1612.     public function getCachedDescendants($absPath$class Node::class)
  1613.     {
  1614.         $descendants = [];
  1615.         foreach ($this->objectsByPath[$class] as $path => $node) {
  1616.             if (=== strpos($path"$absPath/")) {
  1617.                 $descendants[$path] = $node;
  1618.             }
  1619.         }
  1620.         return new ArrayIterator(array_values($descendants));
  1621.     }
  1622.     /**
  1623.      * Get a node if it is already in cache or null otherwise.
  1624.      *
  1625.      * As getCachedNode but looking up the node by uuid.
  1626.      *
  1627.      * Note that this will never return you a removed node because the uuid is
  1628.      * removed from the map.
  1629.      *
  1630.      * @see getCachedNode
  1631.      *
  1632.      * @param $uuid
  1633.      * @param string $class
  1634.      *
  1635.      * @return NodeInterface or null
  1636.      */
  1637.     public function getCachedNodeByUuid($uuid$class Node::class)
  1638.     {
  1639.         if (array_key_exists($uuid$this->objectsByUuid)) {
  1640.             return $this->getCachedNode($this->objectsByUuid[$uuid], $class);
  1641.         }
  1642.         return null;
  1643.     }
  1644.     /**
  1645.      * Purge an item given by path from the cache and return whether the node
  1646.      * should forget it or keep it.
  1647.      *
  1648.      * This is used by Node::refresh() to let the object manager notify
  1649.      * deleted nodes or detect cases when not to delete.
  1650.      *
  1651.      * @param string  $absPath     The absolute path of the item
  1652.      * @param boolean $keepChanges Whether to keep local changes or forget
  1653.      *      them
  1654.      *
  1655.      * @return bool true if the node is to be forgotten by its parent (deleted or
  1656.      *      moved away), false if child should be kept
  1657.      */
  1658.     public function purgeDisappearedNode($absPath$keepChanges)
  1659.     {
  1660.         if (array_key_exists($absPath$this->objectsByPath[Node::class])) {
  1661.             $item $this->objectsByPath[Node::class][$absPath];
  1662.             if ($keepChanges &&
  1663.                 ($item->isNew() || $this->getMoveSrcPath($absPath))
  1664.             ) {
  1665.                 // we keep changes and this is a new node or it moved here
  1666.                 return false;
  1667.             }
  1668.             // may not use $item->getIdentifier here - leads to endless loop if node purges itself
  1669.             $uuid array_search($absPath$this->objectsByUuid);
  1670.             if (false !== $uuid) {
  1671.                 unset($this->objectsByUuid[$uuid]);
  1672.             }
  1673.             unset($this->objectsByPath[Node::class][$absPath]);
  1674.             $item->setDeleted();
  1675.         }
  1676.         // if the node moved away from this node, we did not find it in
  1677.         // objectsByPath and the calling parent node can forget it
  1678.         return true;
  1679.     }
  1680.     /**
  1681.      * Register a given node path against a UUID.
  1682.      *
  1683.      * This is called when setting the UUID property of a node to ensure that
  1684.      * it can be subsequently referenced by the UUID.
  1685.      *
  1686.      * @param string $uuid
  1687.      * @param string $absPath
  1688.      */
  1689.     public function registerUuid($uuid$absPath)
  1690.     {
  1691.         if (array_key_exists($uuid$this->objectsByUuid)) {
  1692.             throw new RuntimeException(sprintf(
  1693.                 'Object path for UUID "%s" has already been registered to "%s"',
  1694.                 $uuid$this->objectsByUuid[$uuid]
  1695.             ));
  1696.         }
  1697.         $this->objectsByUuid[$uuid] = $absPath;
  1698.     }
  1699. }