/** * Push all recorded changes to the backend. * * The order is important to avoid conflicts * 1. remove nodes * 2. move nodes * 3. add new nodes * 4. commit any other changes * * If transactions are enabled but we are not currently inside a * transaction, the session is responsible to start a transaction to make * sure the backend state does not get messed up in case of error. * * @return void */ public function save() { if (!$this->transport instanceof WritingInterface) { throw new UnsupportedRepositoryOperationException('Transport does not support writing'); } // TODO: adjust transport to accept lists and do a diff request instead of single requests /* remove nodes/properties * * deleting a node deletes the whole tree * we have to avoid deleting children/properties of nodes we already * deleted. we sort the paths and then use that to check if parent path * was already removed in a - comparably - cheap way */ $todelete = array_keys($this->itemsRemove); sort($todelete); $last = ':'; // anything but '/' foreach ($todelete as $path) { if (!strncmp($last, $path, strlen($last)) && $path[strlen($last)] == '/') { //parent path has already been removed continue; } if ($this->itemsRemove[$path] instanceof NodeInterface) { $this->transport->deleteNode($path); } elseif ($this->itemsRemove[$path] instanceof PropertyInterface) { $this->transport->deleteProperty($path); } else { throw new RepositoryException("Internal error while deleting {$path}: unknown class " . get_class($this->itemsRemove[$path])); } $last = $path; } // move nodes/properties foreach ($this->nodesMove as $src => $dst) { $this->transport->moveNode($src, $dst); if (isset($this->objectsByPath['Node'][$dst])) { // might not be set if moved again afterwards // move is not treated as modified, need to confirm separately $this->objectsByPath['Node'][$dst]->confirmSaved(); } } // filter out sub-nodes and sub-properties since the top-most nodes that are // added will create all sub-nodes and sub-properties at once $nodesToCreate = $this->itemsAdd; foreach ($nodesToCreate as $path => $dummy) { foreach ($nodesToCreate as $path2 => $dummy) { if (strpos($path2, $path . '/') === 0) { unset($nodesToCreate[$path2]); } } } // create new items foreach ($nodesToCreate as $path => $dummy) { $node = $this->getNodeByPath($path); if (!$node instanceof NodeInterface) { throw new RepositoryException('Internal error: Unknown type ' . get_class($node)); } $this->transport->storeNode($node); } // loop through cached nodes and commit all dirty and set them to clean. if (isset($this->objectsByPath['Node'])) { foreach ($this->objectsByPath['Node'] as $path => $node) { if ($node->isModified()) { if (!$node instanceof NodeInterface) { throw new RepositoryException('Internal Error: Unknown type ' . get_class($node)); } foreach ($node->getProperties() as $property) { if ($property->isModified() || $property->isNew()) { $this->transport->storeProperty($property); } } //order nodes $reorders = $node->getOrderCommands(); if (count($reorders) > 0) { $this->transport->reorderNodes($node->getPath(), $reorders); } } } } $this->transport->finishSave(); //clear those lists before reloading the newly added nodes from backend, to avoid collisions $this->itemsRemove = array(); $this->nodesMove = array(); foreach ($this->itemsAdd as $path => $dummy) { if (!isset($this->objectsByPath['Node'][$path])) { throw new RepositoryException("Internal error: New node was not found in cache '{$path}'"); } $this->objectsByPath['Node'][$path]->confirmSaved(); } if (isset($this->objectsByPath['Node'])) { foreach ($this->objectsByPath['Node'] as $item) { if ($item->isModified() || $item->isMoved()) { $item->confirmSaved(); } } } $this->itemsAdd = array(); }