PageRenderTime 56ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/src/Jackalope/Transport/DoctrineDBAL/Client.php

https://github.com/Nemrtvej/jackalope-doctrine-dbal
PHP | 2281 lines | 1616 code | 296 blank | 369 comment | 146 complexity | 87fc767610c205a536a6ca03803fe2d2 MD5 | raw file
Possible License(s): Apache-2.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. namespace Jackalope\Transport\DoctrineDBAL;
  3. use PHPCR\LoginException;
  4. use PHPCR\NodeType\NodeDefinitionInterface;
  5. use PHPCR\NodeType\NodeTypeDefinitionInterface;
  6. use PHPCR\NodeType\NodeTypeExistsException;
  7. use PHPCR\NodeType\PropertyDefinitionInterface;
  8. use PHPCR\Query\QOM\ColumnInterface;
  9. use PHPCR\Query\QOM\JoinInterface;
  10. use PHPCR\Query\QOM\SourceInterface;
  11. use PHPCR\RepositoryInterface;
  12. use PHPCR\NamespaceRegistryInterface;
  13. use PHPCR\CredentialsInterface;
  14. use PHPCR\PropertyType;
  15. use PHPCR\Query\QOM\QueryObjectModelInterface;
  16. use PHPCR\Query\QOM\SelectorInterface;
  17. use PHPCR\Query\QueryInterface;
  18. use PHPCR\RepositoryException;
  19. use PHPCR\NamespaceException;
  20. use PHPCR\NoSuchWorkspaceException;
  21. use PHPCR\ItemExistsException;
  22. use PHPCR\ItemNotFoundException;
  23. use PHPCR\ReferentialIntegrityException;
  24. use PHPCR\SimpleCredentials;
  25. use PHPCR\Util\ValueConverter;
  26. use PHPCR\ValueFormatException;
  27. use PHPCR\PathNotFoundException;
  28. use PHPCR\Query\InvalidQueryException;
  29. use PHPCR\NodeType\ConstraintViolationException;
  30. use PHPCR\Util\UUIDHelper;
  31. use PHPCR\Util\QOM\Sql2ToQomQueryConverter;
  32. use PHPCR\Util\PathHelper;
  33. use Doctrine\DBAL\Connection;
  34. use Doctrine\DBAL\Driver\PDOConnection;
  35. use Doctrine\DBAL\Platforms\MySqlPlatform;
  36. use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
  37. use Doctrine\DBAL\Platforms\SqlitePlatform;
  38. use Doctrine\DBAL\DBALException;
  39. use Jackalope\Node;
  40. use Jackalope\Property;
  41. use Jackalope\Query\Query;
  42. use Jackalope\Transport\AddNodeOperation;
  43. use Jackalope\Transport\MoveNodeOperation;
  44. use Jackalope\Transport\BaseTransport;
  45. use Jackalope\Transport\QueryInterface as QueryTransport;
  46. use Jackalope\Transport\WritingInterface;
  47. use Jackalope\Transport\WorkspaceManagementInterface;
  48. use Jackalope\Transport\NodeTypeManagementInterface;
  49. use Jackalope\Transport\TransactionInterface;
  50. use Jackalope\Transport\StandardNodeTypes;
  51. use Jackalope\Transport\DoctrineDBAL\Query\QOMWalker;
  52. use Jackalope\NodeType\NodeTypeManager;
  53. use Jackalope\NodeType\NodeType;
  54. use Jackalope\NodeType\NodeTypeDefinition;
  55. use Jackalope\FactoryInterface;
  56. use Jackalope\NotImplementedException;
  57. /**
  58. * Class to handle the communication between Jackalope and RDBMS via Doctrine DBAL.
  59. *
  60. * @license http://www.apache.org/licenses Apache License Version 2.0, January 2004
  61. * @license http://opensource.org/licenses/MIT MIT License
  62. *
  63. * @author Benjamin Eberlei <kontakt@beberlei.de>
  64. * @author Lukas Kahwe Smith <smith@pooteeweet.org>
  65. */
  66. class Client extends BaseTransport implements QueryTransport, WritingInterface, WorkspaceManagementInterface, NodeTypeManagementInterface, TransactionInterface
  67. {
  68. /**
  69. * The factory to instantiate objects
  70. * @var FactoryInterface
  71. */
  72. protected $factory;
  73. /**
  74. * @var ValueConverter
  75. */
  76. protected $valueConverter;
  77. /**
  78. * @var Connection
  79. */
  80. private $conn;
  81. /**
  82. * @var bool
  83. */
  84. private $loggedIn = false;
  85. /**
  86. * @var SimpleCredentials
  87. */
  88. private $credentials;
  89. /**
  90. * @var string
  91. */
  92. protected $workspaceName;
  93. /**
  94. * @var array
  95. */
  96. private $nodeIdentifiers = array();
  97. /**
  98. * @var NodeTypeManager
  99. */
  100. private $nodeTypeManager;
  101. /**
  102. * @var bool
  103. */
  104. protected $inTransaction = false;
  105. /**
  106. * Check if an initial request on login should be send to check if repository exists
  107. * This is according to the JCR specifications and set to true by default
  108. * @see setCheckLoginOnServer
  109. * @var bool
  110. */
  111. private $checkLoginOnServer = true;
  112. /**
  113. * @var array
  114. */
  115. protected $namespaces = array();
  116. /**
  117. * @var string|null
  118. */
  119. private $sequenceWorkspaceName;
  120. /**
  121. * @var string|null
  122. */
  123. private $sequenceNodeName;
  124. /**
  125. * @var string|null
  126. */
  127. private $sequenceTypeName;
  128. /**
  129. * @var array
  130. */
  131. private $referencesToUpdate = array();
  132. /**
  133. * @var array
  134. */
  135. private $referenceTables = array(
  136. PropertyType::REFERENCE => 'phpcr_nodes_references',
  137. PropertyType::WEAKREFERENCE => 'phpcr_nodes_weakreferences',
  138. );
  139. /**
  140. * @var array
  141. */
  142. private $referencesToDelete = array();
  143. /**
  144. * @param FactoryInterface $factory
  145. * @param Connection $conn
  146. */
  147. public function __construct(FactoryInterface $factory, Connection $conn)
  148. {
  149. $this->factory = $factory;
  150. $this->valueConverter = $this->factory->get('PHPCR\Util\ValueConverter');
  151. $this->conn = $conn;
  152. if ($conn->getDatabasePlatform() instanceof PostgreSqlPlatform) {
  153. $this->sequenceWorkspaceName = 'phpcr_workspaces_id_seq';
  154. $this->sequenceNodeName = 'phpcr_nodes_id_seq';
  155. $this->sequenceTypeName = 'phpcr_type_nodes_node_type_id_seq';
  156. }
  157. // @TODO: move to "SqlitePlatform" and rename to "registerExtraFunctions"?
  158. if ($this->conn->getDatabasePlatform() instanceof SqlitePlatform) {
  159. $this->registerSqliteFunctions($this->conn->getWrappedConnection());
  160. }
  161. }
  162. /**
  163. * @TODO: move to "SqlitePlatform" and rename to "registerExtraFunctions"?
  164. *
  165. * @param PDOConnection $sqliteConnection
  166. *
  167. * @return Client
  168. */
  169. private function registerSqliteFunctions(PDOConnection $sqliteConnection)
  170. {
  171. $sqliteConnection->sqliteCreateFunction('EXTRACTVALUE', function ($string, $expression) {
  172. $dom = new \DOMDocument('1.0', 'UTF-8');
  173. $dom->loadXML($string);
  174. $xpath = new \DOMXPath($dom);
  175. $list = $xpath->evaluate($expression);
  176. if (!is_object($list)) {
  177. return $list;
  178. }
  179. // @TODO: don't know if there are expressions returning more then one row
  180. if ($list->length > 0) {
  181. return $list->item(0)->textContent;
  182. }
  183. // @TODO: don't know if return value is right
  184. return null;
  185. }, 2);
  186. $sqliteConnection->sqliteCreateFunction('CONCAT', function () {
  187. return implode('', func_get_args());
  188. });
  189. return $this;
  190. }
  191. /**
  192. * @return Connection
  193. */
  194. public function getConnection()
  195. {
  196. return $this->conn;
  197. }
  198. /**
  199. * {@inheritDoc}
  200. *
  201. */
  202. public function createWorkspace($name, $srcWorkspace = null)
  203. {
  204. if (null !== $srcWorkspace) {
  205. throw new NotImplementedException('Creating workspace as clone of existing workspace not supported');
  206. }
  207. if ($this->workspaceExists($name)) {
  208. throw new RepositoryException("Workspace '$name' already exists");
  209. }
  210. try {
  211. $this->conn->insert('phpcr_workspaces', array('name' => $name));
  212. } catch (\Exception $e) {
  213. throw new RepositoryException("Couldn't create Workspace '$name': ".$e->getMessage(), 0, $e);
  214. }
  215. $this->conn->insert('phpcr_nodes', array(
  216. 'path' => '/',
  217. 'parent' => '',
  218. 'workspace_name'=> $name,
  219. 'identifier' => UUIDHelper::generateUUID(),
  220. 'type' => 'nt:unstructured',
  221. 'local_name' => '',
  222. 'namespace' => '',
  223. 'props' => '<?xml version="1.0" encoding="UTF-8"?>
  224. <sv:node xmlns:mix="http://www.jcp.org/jcr/mix/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:sv="http://www.jcp.org/jcr/sv/1.0" xmlns:rep="internal" />',
  225. // TODO compute proper value
  226. 'depth' => 0,
  227. ));
  228. }
  229. /**
  230. * {@inheritDoc}
  231. */
  232. public function deleteWorkspace($name)
  233. {
  234. if (!$this->workspaceExists($name)) {
  235. throw new RepositoryException("Workspace '$name' cannot be
  236. deleted as it does not exist");
  237. }
  238. try {
  239. $this->conn->delete('phpcr_workspaces', array('name' => $name));
  240. } catch (\Exception $e) {
  241. throw new RepositoryException("Couldn't delete workspace '$name': "
  242. .$e->getMessage(), 0, $e);
  243. }
  244. try {
  245. $this->conn->delete('phpcr_nodes', array('workspace_name'=>
  246. $name));
  247. } catch (\Exception $e) {
  248. throw new RepositoryException("Couldn't delete nodes in workspace
  249. '$name': ".$e->getMessage(), 0, $e);
  250. }
  251. try {
  252. $this->conn->delete('phpcr_binarydata', array('workspace_name'=>
  253. $name));
  254. } catch (\Exception $e) {
  255. throw new RepositoryException("Couldn't delete binary data in
  256. workspace '$name': ".$e->getMessage(), 0, $e);
  257. }
  258. }
  259. /**
  260. * {@inheritDoc}
  261. */
  262. public function login(CredentialsInterface $credentials = null, $workspaceName = null)
  263. {
  264. $this->credentials = $credentials;
  265. if (empty($workspaceName)) {
  266. $workspaceName = 'default';
  267. }
  268. $this->workspaceName = $workspaceName;
  269. if (!$this->checkLoginOnServer) {
  270. return $workspaceName;
  271. }
  272. if (!$this->workspaceExists($workspaceName)) {
  273. if ('default' !== $workspaceName) {
  274. throw new NoSuchWorkspaceException("Requested workspace: '$workspaceName'");
  275. }
  276. // create default workspace if it not exists
  277. $this->createWorkspace($workspaceName);
  278. }
  279. $this->loggedIn = true;
  280. return $workspaceName;
  281. }
  282. /**
  283. * {@inheritDoc}
  284. */
  285. public function logout()
  286. {
  287. if ($this->loggedIn) {
  288. $this->loggedIn = false;
  289. $this->conn->close();
  290. $this->conn = null;
  291. }
  292. }
  293. /**
  294. * Configure whether to check if we are logged in before doing a request.
  295. *
  296. * Will improve error reporting at the cost of some round trips.
  297. */
  298. public function setCheckLoginOnServer($bool)
  299. {
  300. $this->checkLoginOnServer = $bool;
  301. }
  302. protected function workspaceExists($workspaceName)
  303. {
  304. try {
  305. $query = 'SELECT 1 FROM phpcr_workspaces WHERE name = ?';
  306. $result = $this->conn->fetchColumn($query, array($workspaceName));
  307. } catch (\Exception $e) {
  308. if ($e instanceof DBALException || $e instanceof \PDOException) {
  309. if (1045 == $e->getCode()) {
  310. throw new LoginException('Access denied with your credentials: '.$e->getMessage());
  311. }
  312. if ('42S02' == $e->getCode()) {
  313. throw new RepositoryException('You did not properly set up the database for the repository. See README.md for more information. Message from backend: '.$e->getMessage());
  314. }
  315. throw new RepositoryException('Unexpected error talking to the backend: '.$e->getMessage());
  316. }
  317. throw $e;
  318. }
  319. return $result;
  320. }
  321. /**
  322. * Ensure that we are currently logged in, executing the login in case we
  323. * did lazy login.
  324. *
  325. * @throws RepositoryException if this transport is not logged in.
  326. */
  327. protected function assertLoggedIn()
  328. {
  329. if (!$this->loggedIn) {
  330. if (!$this->checkLoginOnServer && $this->workspaceName) {
  331. $this->checkLoginOnServer = true;
  332. if ($this->login($this->credentials, $this->workspaceName)) {
  333. return;
  334. }
  335. }
  336. throw new RepositoryException('You need to be logged in for this operation');
  337. }
  338. }
  339. /**
  340. * {@inheritDoc}
  341. */
  342. public function getRepositoryDescriptors()
  343. {
  344. return array(
  345. RepositoryInterface::IDENTIFIER_STABILITY => RepositoryInterface::IDENTIFIER_STABILITY_INDEFINITE_DURATION,
  346. RepositoryInterface::REP_NAME_DESC => 'jackalope_doctrine_dbal',
  347. RepositoryInterface::REP_VENDOR_DESC => 'Jackalope Community',
  348. RepositoryInterface::REP_VENDOR_URL_DESC => 'http://github.com/jackalope',
  349. RepositoryInterface::REP_VERSION_DESC => '1.0.0-DEV',
  350. RepositoryInterface::SPEC_NAME_DESC => 'Content Repository for PHP',
  351. RepositoryInterface::SPEC_VERSION_DESC => '2.1',
  352. RepositoryInterface::NODE_TYPE_MANAGEMENT_AUTOCREATED_DEFINITIONS_SUPPORTED => true,
  353. RepositoryInterface::NODE_TYPE_MANAGEMENT_INHERITANCE => RepositoryInterface::NODE_TYPE_MANAGEMENT_INHERITANCE_SINGLE,
  354. RepositoryInterface::NODE_TYPE_MANAGEMENT_MULTIPLE_BINARY_PROPERTIES_SUPPORTED => true,
  355. RepositoryInterface::NODE_TYPE_MANAGEMENT_MULTIVALUED_PROPERTIES_SUPPORTED => true,
  356. RepositoryInterface::NODE_TYPE_MANAGEMENT_ORDERABLE_CHILD_NODES_SUPPORTED => true,
  357. RepositoryInterface::NODE_TYPE_MANAGEMENT_OVERRIDES_SUPPORTED => false,
  358. RepositoryInterface::NODE_TYPE_MANAGEMENT_PRIMARY_ITEM_NAME_SUPPORTED => true,
  359. RepositoryInterface::NODE_TYPE_MANAGEMENT_PROPERTY_TYPES => true,
  360. RepositoryInterface::NODE_TYPE_MANAGEMENT_RESIDUAL_DEFINITIONS_SUPPORTED => false,
  361. RepositoryInterface::NODE_TYPE_MANAGEMENT_SAME_NAME_SIBLINGS_SUPPORTED => false,
  362. RepositoryInterface::NODE_TYPE_MANAGEMENT_UPDATE_IN_USE_SUPPORTED => false,
  363. RepositoryInterface::NODE_TYPE_MANAGEMENT_VALUE_CONSTRAINTS_SUPPORTED => false,
  364. RepositoryInterface::OPTION_ACCESS_CONTROL_SUPPORTED => false,
  365. RepositoryInterface::OPTION_ACTIVITIES_SUPPORTED => false,
  366. RepositoryInterface::OPTION_BASELINES_SUPPORTED => false,
  367. RepositoryInterface::OPTION_JOURNALED_OBSERVATION_SUPPORTED => false,
  368. RepositoryInterface::OPTION_LIFECYCLE_SUPPORTED => false,
  369. RepositoryInterface::OPTION_LOCKING_SUPPORTED => false,
  370. RepositoryInterface::OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED => true,
  371. RepositoryInterface::OPTION_NODE_TYPE_MANAGEMENT_SUPPORTED => true,
  372. RepositoryInterface::OPTION_OBSERVATION_SUPPORTED => false,
  373. RepositoryInterface::OPTION_RETENTION_SUPPORTED => false,
  374. RepositoryInterface::OPTION_SHAREABLE_NODES_SUPPORTED => false,
  375. RepositoryInterface::OPTION_SIMPLE_VERSIONING_SUPPORTED => false,
  376. RepositoryInterface::OPTION_TRANSACTIONS_SUPPORTED => true,
  377. RepositoryInterface::OPTION_UNFILED_CONTENT_SUPPORTED => true,
  378. RepositoryInterface::OPTION_UPDATE_MIXIN_NODETYPES_SUPPORTED => true,
  379. RepositoryInterface::OPTION_UPDATE_PRIMARY_NODETYPE_SUPPORTED => true,
  380. RepositoryInterface::OPTION_VERSIONING_SUPPORTED => false,
  381. RepositoryInterface::OPTION_WORKSPACE_MANAGEMENT_SUPPORTED => true,
  382. RepositoryInterface::OPTION_XML_EXPORT_SUPPORTED => true,
  383. RepositoryInterface::OPTION_XML_IMPORT_SUPPORTED => true,
  384. RepositoryInterface::QUERY_FULL_TEXT_SEARCH_SUPPORTED => true,
  385. RepositoryInterface::QUERY_CANCEL_SUPPORTED => false,
  386. RepositoryInterface::QUERY_JOINS => RepositoryInterface::QUERY_JOINS_NONE,
  387. RepositoryInterface::QUERY_LANGUAGES => array(QueryInterface::JCR_SQL2, QueryInterface::JCR_JQOM),
  388. RepositoryInterface::QUERY_STORED_QUERIES_SUPPORTED => false,
  389. RepositoryInterface::WRITE_SUPPORTED => true,
  390. );
  391. }
  392. /**
  393. * {@inheritDoc}
  394. */
  395. public function getNamespaces()
  396. {
  397. if (empty($this->namespaces)) {
  398. $query = 'SELECT * FROM phpcr_namespaces';
  399. $data = $this->conn->fetchAll($query);
  400. $this->namespaces = array(
  401. NamespaceRegistryInterface::PREFIX_EMPTY => NamespaceRegistryInterface::NAMESPACE_EMPTY,
  402. NamespaceRegistryInterface::PREFIX_JCR => NamespaceRegistryInterface::NAMESPACE_JCR,
  403. NamespaceRegistryInterface::PREFIX_NT => NamespaceRegistryInterface::NAMESPACE_NT,
  404. NamespaceRegistryInterface::PREFIX_MIX => NamespaceRegistryInterface::NAMESPACE_MIX,
  405. NamespaceRegistryInterface::PREFIX_XML => NamespaceRegistryInterface::NAMESPACE_XML,
  406. NamespaceRegistryInterface::PREFIX_SV => NamespaceRegistryInterface::NAMESPACE_SV,
  407. );
  408. foreach ($data as $row) {
  409. $this->namespaces[$row['prefix']] = $row['uri'];
  410. }
  411. }
  412. return $this->namespaces;
  413. }
  414. /**
  415. * {@inheritDoc}
  416. */
  417. public function copyNode($srcAbsPath, $dstAbsPath, $srcWorkspace = null)
  418. {
  419. $this->assertLoggedIn();
  420. $workspaceName = $this->workspaceName;
  421. if (null !== $srcWorkspace) {
  422. if (!$this->workspaceExists($srcWorkspace)) {
  423. throw new NoSuchWorkspaceException("Source workspace '$srcWorkspace' does not exist.");
  424. }
  425. }
  426. PathHelper::assertValidAbsolutePath($dstAbsPath, true);
  427. $srcNodeId = $this->pathExists($srcAbsPath);
  428. if (!$srcNodeId) {
  429. throw new PathNotFoundException("Source path '$srcAbsPath' not found");
  430. }
  431. if ($this->pathExists($dstAbsPath)) {
  432. throw new ItemExistsException("Cannot copy to destination path '$dstAbsPath' that already exists.");
  433. }
  434. if (!$this->pathExists(PathHelper::getParentPath($dstAbsPath))) {
  435. throw new PathNotFoundException("Parent of the destination path '" . $dstAbsPath . "' has to exist.");
  436. }
  437. // Algorithm:
  438. // 1. Select all nodes with path $srcAbsPath."%" and iterate them
  439. // 2. create a new node with path $dstAbsPath + leftovers, with a new uuid. Save old => new uuid
  440. // 3. copy all properties from old node to new node
  441. // 4. if a reference is in the properties, either update the uuid based on the map if its inside the copied graph or keep it.
  442. // 5. "May drop mixin types"
  443. $query = 'SELECT * FROM phpcr_nodes WHERE path LIKE ? AND workspace_name = ?';
  444. $stmt = $this->conn->executeQuery($query, array($srcAbsPath . '%', $workspaceName));
  445. foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
  446. $newPath = str_replace($srcAbsPath, $dstAbsPath, $row['path']);
  447. $dom = new \DOMDocument('1.0', 'UTF-8');
  448. $dom->loadXML($row['props']);
  449. $propsData = array('dom' => $dom);
  450. // when copying a node, the copy is always a new node. set $isNewNode to true
  451. $newNodeId = $this->syncNode(null, $newPath, $row['type'], true, array(), $propsData);
  452. $query = 'INSERT INTO phpcr_binarydata (node_id, property_name, workspace_name, idx, data)'.
  453. ' SELECT ?, b.property_name, ?, b.idx, b.data FROM phpcr_binarydata b WHERE b.node_id = ?';
  454. try {
  455. $this->conn->executeUpdate($query, array($newNodeId, $this->workspaceName, $srcNodeId));
  456. } catch (DBALException $e) {
  457. throw new RepositoryException("Unexpected exception while copying node from $srcAbsPath to $dstAbsPath", $e->getCode(), $e);
  458. }
  459. }
  460. }
  461. /**
  462. * @param string $path
  463. *
  464. * @return array
  465. */
  466. private function getJcrName($path)
  467. {
  468. $name = implode('', array_slice(explode('/', $path), -1, 1));
  469. if (($aliasLength = strpos($name, ':')) !== false) {
  470. $alias = substr($name, 0, $aliasLength);
  471. $name = substr($name, $aliasLength + 1);
  472. } else {
  473. $alias = '';
  474. }
  475. $namespaces = $this->getNamespaces();
  476. if (!isset($namespaces[$alias])) {
  477. throw new NamespaceException('the namespace ' . $alias . ' was not registered.');
  478. }
  479. return array($namespaces[$alias], $name);
  480. }
  481. /**
  482. * Actually write the node into the database
  483. *
  484. * @param string $uuid node uuid
  485. * @param string $path absolute path of the node
  486. * @param string $type node type
  487. * @param boolean $isNewNode new nodes to insert (true) or existing node to update (false)
  488. * @param array $props
  489. * @param array $propsData
  490. *
  491. * @return boolean|string
  492. *
  493. * @throws ItemExistsException
  494. * @throws RepositoryException
  495. */
  496. private function syncNode($uuid, $path, $type, $isNewNode, $props = array(), $propsData = array())
  497. {
  498. // TODO: Not sure if there are always ALL props in $props, should we grab the online data here?
  499. // TODO: Binary data is handled very inefficiently here, UPSERT will really be necessary here as well as lazy handling
  500. if (!$propsData) {
  501. $propsData = $this->propsToXML($props);
  502. }
  503. if (null === $uuid) {
  504. $uuid = UUIDHelper::generateUUID();
  505. }
  506. if ($isNewNode) {
  507. list($namespace, $localName) = $this->getJcrName($path);
  508. $qb = $this->conn->createQueryBuilder();
  509. $qb->select(':identifier, :type, :path, :local_name, :namespace, :parent, :workspace_name, :props, :depth, COALESCE(MAX(n.sort_order), 0) + 1')
  510. ->from('phpcr_nodes', 'n')
  511. ->where('n.parent = :parent_a');
  512. $sql = $qb->getSql();
  513. try {
  514. $insert = "INSERT INTO phpcr_nodes (identifier, type, path, local_name, namespace, parent, workspace_name, props, depth, sort_order) " . $sql;
  515. $this->conn->executeUpdate($insert, array(
  516. 'identifier' => $uuid,
  517. 'type' => $type,
  518. 'path' => $path,
  519. 'local_name' => $localName,
  520. 'namespace' => $namespace,
  521. 'parent' => PathHelper::getParentPath($path),
  522. 'workspace_name' => $this->workspaceName,
  523. 'props' => $propsData['dom']->saveXML(),
  524. 'depth' => PathHelper::getPathDepth($path),
  525. 'parent_a' => PathHelper::getParentPath($path),
  526. ));
  527. } catch (\PDOException $e) {
  528. throw new ItemExistsException('Item ' . $path . ' already exists in the database');
  529. } catch (DBALException $e) {
  530. throw new ItemExistsException('Item ' . $path . ' already exists in the database');
  531. }
  532. $nodeId = $this->conn->lastInsertId($this->sequenceNodeName);
  533. } else {
  534. $nodeId = $this->pathExists($path);
  535. if (!$nodeId) {
  536. throw new RepositoryException("nodeId for $path not found");
  537. }
  538. $this->conn->update('phpcr_nodes', array('props' => $propsData['dom']->saveXML()), array('id' => $nodeId));
  539. }
  540. $this->nodeIdentifiers[$path] = $uuid;
  541. if (!empty($propsData['binaryData'])) {
  542. $this->syncBinaryData($nodeId, $propsData['binaryData']);
  543. }
  544. if (!empty($propsData['references'])) {
  545. $this->referencesToUpdate[$nodeId] = array('path' => $path, 'properties' => $propsData['references']);
  546. }
  547. return $nodeId;
  548. }
  549. private function syncBinaryData($nodeId, $binaryData)
  550. {
  551. foreach ($binaryData as $propertyName => $binaryValues) {
  552. foreach ($binaryValues as $idx => $data) {
  553. // TODO verify in which cases we can just update
  554. $params = array(
  555. 'node_id' => $nodeId,
  556. 'property_name' => $propertyName,
  557. 'workspace_name' => $this->workspaceName,
  558. );
  559. $this->conn->delete('phpcr_binarydata', $params);
  560. $params['idx'] = $idx;
  561. $params['data'] = $data;
  562. $types = array(
  563. \PDO::PARAM_INT,
  564. \PDO::PARAM_STR,
  565. \PDO::PARAM_STR,
  566. \PDO::PARAM_INT,
  567. \PDO::PARAM_LOB
  568. );
  569. $this->conn->insert('phpcr_binarydata', $params, $types);
  570. }
  571. }
  572. }
  573. private function syncReferences()
  574. {
  575. if ($this->referencesToUpdate) {
  576. // do not update references that are going to be deleted anyways
  577. $toUpdate = array_diff_assoc($this->referencesToUpdate, $this->referencesToDelete);
  578. try {
  579. foreach ($this->referenceTables as $table) {
  580. $query = "DELETE FROM $table WHERE source_id IN (?)";
  581. $this->conn->executeUpdate($query, array(array_keys($toUpdate)), array(Connection::PARAM_INT_ARRAY));
  582. }
  583. } catch (DBALException $e) {
  584. throw new RepositoryException('Unexpected exception while cleaning up after saving', $e->getCode(), $e);
  585. }
  586. foreach ($toUpdate as $nodeId => $references) {
  587. foreach ($references['properties'] as $name => $data) {
  588. foreach ($data['values'] as $value) {
  589. try {
  590. $params = array(
  591. 'source_id' => $nodeId,
  592. 'source_property_name' => $name,
  593. 'target_id' => $this->pathExists(self::getNodePathForIdentifier($value)),
  594. );
  595. $this->conn->insert($this->referenceTables[$data['type']], $params);
  596. } catch (ItemNotFoundException $e) {
  597. if (PropertyType::REFERENCE === $data['type']) {
  598. throw new ReferentialIntegrityException(
  599. "Trying to store reference to non-existant node with path '$value' in node '{$references['path']}' property '$name'."
  600. );
  601. }
  602. }
  603. }
  604. }
  605. }
  606. }
  607. // TODO on RDBMS that support deferred FKs we could skip this step
  608. if ($this->referencesToDelete) {
  609. $params = array(array_keys($this->referencesToDelete));
  610. // remove all PropertyType::REFERENCE with a source_id on a deleted node
  611. try {
  612. $query = "DELETE FROM phpcr_nodes_references WHERE source_id IN (?)";
  613. $this->conn->executeUpdate($query, $params, array(Connection::PARAM_INT_ARRAY));
  614. } catch (DBALException $e) {
  615. throw new RepositoryException('Unexpected exception while cleaning up deleted nodes', $e->getCode(), $e);
  616. }
  617. // ensure that there are no PropertyType::REFERENCE pointing to nodes that will be deleted
  618. // Note: due to the outer join we cannot filter on workspace_name, but this is ok
  619. // since within a transaction there can never be missing referenced nodes within the current workspace
  620. // make sure the target node is not in the list of nodes being deleted, to allow deletion in same request
  621. $query = 'SELECT DISTINCT r.target_id
  622. FROM phpcr_nodes_references r
  623. LEFT OUTER JOIN phpcr_nodes n ON r.target_id = n.id
  624. WHERE r.target_id IN (?)';
  625. $stmt = $this->conn->executeQuery($query, $params, array(Connection::PARAM_INT_ARRAY));
  626. $missingTargets = $stmt->fetchAll(\PDO::FETCH_COLUMN);
  627. if ($missingTargets) {
  628. $paths = array();
  629. foreach ($missingTargets as $id) {
  630. if (isset($this->referencesToDelete[$id])) {
  631. $paths[] = $this->referencesToDelete[$id];
  632. }
  633. }
  634. throw new ReferentialIntegrityException("Cannot delete '".implode("', '", $paths)."': A reference points to this node or a subnode");
  635. }
  636. // clean up all references
  637. try {
  638. foreach ($this->referenceTables as $table) {
  639. $query = "DELETE FROM $table WHERE target_id IN (?)";
  640. $this->conn->executeUpdate($query, $params, array(Connection::PARAM_INT_ARRAY));
  641. }
  642. } catch (DBALException $e) {
  643. throw new RepositoryException('Unexpected exception while cleaning up deleted nodes', $e->getCode(), $e);
  644. }
  645. }
  646. }
  647. public static function xmlToProps($xml, ValueConverter $valueConverter, $filter = null)
  648. {
  649. $data = new \stdClass();
  650. $dom = new \DOMDocument('1.0', 'UTF-8');
  651. $dom->loadXML($xml);
  652. foreach ($dom->getElementsByTagNameNS('http://www.jcp.org/jcr/sv/1.0', 'property') as $propertyNode) {
  653. $name = $propertyNode->getAttribute('sv:name');
  654. // only return the properties that pass through the filter callback
  655. if (null !== $filter && is_callable($filter) && false === $filter($name)) {
  656. continue;
  657. }
  658. $values = array();
  659. $type = PropertyType::valueFromName($propertyNode->getAttribute('sv:type'));
  660. foreach ($propertyNode->childNodes as $valueNode) {
  661. switch ($type) {
  662. case PropertyType::NAME:
  663. case PropertyType::URI:
  664. case PropertyType::WEAKREFERENCE:
  665. case PropertyType::REFERENCE:
  666. case PropertyType::PATH:
  667. case PropertyType::DECIMAL:
  668. case PropertyType::STRING:
  669. $values[] = $valueNode->nodeValue;
  670. break;
  671. case PropertyType::BOOLEAN:
  672. $values[] = (bool) $valueNode->nodeValue;
  673. break;
  674. case PropertyType::LONG:
  675. $values[] = (int) $valueNode->nodeValue;
  676. break;
  677. case PropertyType::BINARY:
  678. $values[] = (int) $valueNode->nodeValue;
  679. break;
  680. case PropertyType::DATE:
  681. $date = $valueNode->nodeValue;
  682. if ($date) {
  683. $date = new \DateTime($date);
  684. $date->setTimezone(new \DateTimeZone(date_default_timezone_get()));
  685. // Jackalope expects a string, might make sense to refactor to allow \DateTime instances too
  686. $date = $valueConverter->convertType($date, PropertyType::STRING);
  687. }
  688. $values[] = $date;
  689. break;
  690. case PropertyType::DOUBLE:
  691. $values[] = (double) $valueNode->nodeValue;
  692. break;
  693. default:
  694. throw new \InvalidArgumentException("Type with constant $type not found.");
  695. }
  696. }
  697. switch ($type) {
  698. case PropertyType::BINARY:
  699. $data->{':' . $name} = $propertyNode->getAttribute('sv:multi-valued') ? $values : $values[0];
  700. break;
  701. default:
  702. $data->{$name} = $propertyNode->getAttribute('sv:multi-valued') ? $values : $values[0];
  703. $data->{':' . $name} = $type;
  704. break;
  705. }
  706. }
  707. return $data;
  708. }
  709. /**
  710. * Seperate properties array into an xml and binary data.
  711. *
  712. * @param array $properties
  713. * @param boolean $inlineBinaries
  714. *
  715. * @return array ('dom' => $dom, 'binaryData' => streams, 'references' => array('type' => INT, 'values' => array(UUIDs)))
  716. */
  717. private function propsToXML($properties, $inlineBinaries = false)
  718. {
  719. $namespaces = array(
  720. 'mix' => "http://www.jcp.org/jcr/mix/1.0",
  721. 'nt' => "http://www.jcp.org/jcr/nt/1.0",
  722. 'xs' => "http://www.w3.org/2001/XMLSchema",
  723. 'jcr' => "http://www.jcp.org/jcr/1.0",
  724. 'sv' => "http://www.jcp.org/jcr/sv/1.0",
  725. 'rep' => "internal"
  726. );
  727. $dom = new \DOMDocument('1.0', 'UTF-8');
  728. $rootNode = $dom->createElement('sv:node');
  729. foreach ($namespaces as $namespace => $uri) {
  730. $rootNode->setAttribute('xmlns:' . $namespace, $uri);
  731. }
  732. $dom->appendChild($rootNode);
  733. $binaryData = $references = array();
  734. foreach ($properties as $property) {
  735. /* @var $property Property */
  736. $propertyNode = $dom->createElement('sv:property');
  737. $propertyNode->setAttribute('sv:name', $property->getName());
  738. $propertyNode->setAttribute('sv:type', PropertyType::nameFromValue($property->getType()));
  739. $propertyNode->setAttribute('sv:multi-valued', $property->isMultiple() ? '1' : '0');
  740. switch ($property->getType()) {
  741. case PropertyType::WEAKREFERENCE:
  742. case PropertyType::REFERENCE:
  743. $references[$property->getName()] = array(
  744. 'type' => $property->getType(),
  745. 'values' => $property->isMultiple() ? array_unique($property->getString()) : array($property->getString()),
  746. );
  747. case PropertyType::NAME:
  748. case PropertyType::URI:
  749. case PropertyType::PATH:
  750. case PropertyType::STRING:
  751. $values = $property->getString();
  752. break;
  753. case PropertyType::DECIMAL:
  754. $values = $property->getDecimal();
  755. break;
  756. case PropertyType::BOOLEAN:
  757. $values = array_map('intval', (array) $property->getBoolean());
  758. break;
  759. case PropertyType::LONG:
  760. $values = $property->getLong();
  761. break;
  762. case PropertyType::BINARY:
  763. if ($property->isNew() || $property->isModified()) {
  764. $values = array();
  765. foreach ((array) $property->getValueForStorage() as $stream) {
  766. if (null === $stream) {
  767. $binary = '';
  768. } else {
  769. $binary = stream_get_contents($stream);
  770. fclose($stream);
  771. }
  772. $binaryData[$property->getName()][] = $binary;
  773. $values[] = strlen($binary);
  774. }
  775. } else {
  776. $values = $property->getLength();
  777. if (!$property->isMultiple() && empty($values)) {
  778. $values = array(0);
  779. }
  780. }
  781. break;
  782. case PropertyType::DATE:
  783. $values = $property->getDate();
  784. if ($values instanceof \DateTime) {
  785. $values = array($values);
  786. }
  787. foreach ((array) $values as $key => $date) {
  788. if ($date instanceof \DateTime) {
  789. $date->setTimezone(new \DateTimeZone('UTC'));
  790. }
  791. $values[$key] = $date;
  792. }
  793. $values = $this->valueConverter->convertType($values, PropertyType::STRING);
  794. break;
  795. case PropertyType::DOUBLE:
  796. $values = $property->getDouble();
  797. break;
  798. default:
  799. throw new RepositoryException('unknown type '.$property->getType());
  800. }
  801. foreach ((array) $values as $value) {
  802. $element = $propertyNode->appendChild($dom->createElement('sv:value'));
  803. $element->appendChild($dom->createTextNode($value));
  804. }
  805. $rootNode->appendChild($propertyNode);
  806. }
  807. return array('dom' => $dom, 'binaryData' => $binaryData, 'references' => $references);
  808. }
  809. /**
  810. * {@inheritDoc}
  811. */
  812. public function getAccessibleWorkspaceNames()
  813. {
  814. $query = "SELECT DISTINCT name FROM phpcr_workspaces";
  815. $stmt = $this->conn->executeQuery($query);
  816. return $stmt->fetchAll(\PDO::FETCH_COLUMN);
  817. }
  818. /**
  819. * {@inheritDoc}
  820. */
  821. public function getNode($path)
  822. {
  823. $this->assertLoggedIn();
  824. PathHelper::assertValidAbsolutePath($path);
  825. $values[':path'] = $path;
  826. $values[':pathd'] = rtrim($path,'/') . '/%';
  827. $values[':workspace'] = $this->workspaceName;
  828. $values[':fetchDepth'] = $this->fetchDepth;
  829. $query = 'SELECT * FROM phpcr_nodes
  830. WHERE (path LIKE :pathd OR path = :path)
  831. AND workspace_name = :workspace
  832. AND depth <= ((SELECT depth FROM phpcr_nodes WHERE path = :path AND workspace_name = :workspace) + :fetchDepth)
  833. ORDER BY sort_order ASC';
  834. $stmt = $this->conn->executeQuery($query, $values);
  835. $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
  836. $nodeData = array();
  837. foreach ($rows as $row) {
  838. if ($row['path'] === $path) {
  839. $node = $this->getNodeData($path, $row);
  840. } else {
  841. $pathDiff = ltrim(substr($row['path'], strlen($path)),'/');
  842. $nodeData[$pathDiff] = $this->getNodeData($row['path'], $row);
  843. }
  844. }
  845. if (empty($node)) {
  846. throw new ItemNotFoundException("Item $path not found in workspace ".$this->workspaceName);
  847. }
  848. foreach ($nodeData as $key => $value) {
  849. $node->{$key} = $value;
  850. }
  851. return $node;
  852. }
  853. private function getNodeData($path, $row)
  854. {
  855. $this->nodeIdentifiers[$path] = $row['identifier'];
  856. $data = self::xmlToProps($row['props'], $this->valueConverter);
  857. $data->{'jcr:primaryType'} = $row['type'];
  858. $query = 'SELECT path FROM phpcr_nodes WHERE parent = ? AND workspace_name = ? ORDER BY sort_order ASC';
  859. $children = $this->conn->fetchAll($query, array($path, $this->workspaceName));
  860. foreach ($children as $child) {
  861. $childName = explode('/', $child['path']);
  862. $childName = end($childName);
  863. if (!isset($data->{$childName})) {
  864. $data->{$childName} = new \stdClass();
  865. }
  866. }
  867. // If the node is referenceable, return jcr:uuid.
  868. if (isset($data->{"jcr:mixinTypes"})) {
  869. foreach ((array) $data->{"jcr:mixinTypes"} as $mixin) {
  870. if ($this->nodeTypeManager->getNodeType($mixin)->isNodeType('mix:referenceable')) {
  871. $data->{'jcr:uuid'} = $row['identifier'];
  872. break;
  873. }
  874. }
  875. }
  876. return $data;
  877. }
  878. /**
  879. * {@inheritDoc}
  880. */
  881. public function getNodes($paths)
  882. {
  883. $this->assertLoggedIn();
  884. if (empty($paths)) {
  885. return array();
  886. }
  887. foreach ($paths as $path) {
  888. PathHelper::assertValidAbsolutePath($path);
  889. }
  890. $params[':workspace'] = $this->workspaceName;
  891. $params[':fetchDepth'] = $this->fetchDepth;
  892. $query = 'SELECT path AS arraykey, id, path, parent, local_name, namespace, workspace_name, identifier, type, props, depth, sort_order
  893. FROM phpcr_nodes WHERE workspace_name = :workspace AND (';
  894. $i = 0;
  895. foreach ($paths as $path) {
  896. $params[':path'.$i] = $path;
  897. $params[':pathd'.$i] = rtrim($path,'/') . '/%';
  898. $subquery = 'SELECT depth FROM phpcr_nodes WHERE path = :path'.$i.' AND workspace_name = :workspace';
  899. $query .= '(path LIKE :pathd'.$i.' OR path = :path'.$i.') AND depth <= ((' . $subquery . ') + :fetchDepth) OR ';
  900. $i++;
  901. }
  902. $query = rtrim($query, 'OR ');
  903. $query .= ') ORDER BY sort_order ASC';
  904. $stmt = $this->conn->executeQuery($query, $params);
  905. $all = $stmt->fetchAll(\PDO::FETCH_UNIQUE | \PDO::FETCH_GROUP);
  906. $nodes = array();
  907. foreach ($paths as $path) {
  908. if (isset($all[$path])) {
  909. $nodes[$path] = $this->getNodeData($path, $all[$path]);
  910. }
  911. }
  912. return $nodes;
  913. }
  914. private function pathExists($path)
  915. {
  916. $query = 'SELECT id FROM phpcr_nodes WHERE path = ? AND workspace_name = ?';
  917. if ($nodeId = $this->conn->fetchColumn($query, array($path, $this->workspaceName))) {
  918. return $nodeId;
  919. }
  920. return false;
  921. }
  922. /**
  923. * {@inheritDoc}
  924. */
  925. public function getNodeByIdentifier($uuid)
  926. {
  927. $this->assertLoggedIn();
  928. $query = 'SELECT * FROM phpcr_nodes WHERE identifier = ? AND workspace_name = ?';
  929. $row = $this->conn->fetchAssoc($query, array($uuid, $this->workspaceName));
  930. if (!$row) {
  931. throw new ItemNotFoundException("Item $uuid not found in workspace ".$this->workspaceName);
  932. }
  933. $path = $row['path'];
  934. $data = $this->getNodeData($path, $row);
  935. $data->{':jcr:path'} = $path;
  936. return $data;
  937. }
  938. /**
  939. * {@inheritDoc}
  940. */
  941. public function getNodesByIdentifier($identifiers)
  942. {
  943. $this->assertLoggedIn();
  944. if (empty($identifiers)) {
  945. return array();
  946. }
  947. $query = 'SELECT identifier AS arraykey, id, path, parent, local_name, namespace, workspace_name, identifier, type, props, depth, sort_order
  948. FROM phpcr_nodes WHERE workspace_name = ? AND identifier IN (?)';
  949. $params = array($this->workspaceName, $identifiers);
  950. $stmt = $this->conn->executeQuery($query, $params, array(\PDO::PARAM_STR, Connection::PARAM_STR_ARRAY));
  951. $all = $stmt->fetchAll(\PDO::FETCH_UNIQUE | \PDO::FETCH_GROUP);
  952. $nodes = array();
  953. foreach ($identifiers as $id) {
  954. if (isset($all[$id])) {
  955. $path = $all[$id]['path'];
  956. $nodes[$path] = $this->getNodeData($path, $all[$id]);
  957. }
  958. }
  959. return $nodes;
  960. }
  961. /**
  962. * {@inheritDoc}
  963. */
  964. public function getNodePathForIdentifier($uuid, $workspace = null)
  965. {
  966. if (null !== $workspace) {
  967. throw new NotImplementedException('Specifying the workspace is not yet supported.');
  968. }
  969. $this->assertLoggedIn();
  970. $query = "SELECT path FROM phpcr_nodes WHERE identifier = ? AND workspace_name = ?";
  971. $path = $this->conn->fetchColumn($query, array($uuid, $this->workspaceName));
  972. if (!$path) {
  973. throw new ItemNotFoundException("no item found with uuid ".$uuid);
  974. }
  975. return $path;
  976. }
  977. /**
  978. * {@inheritDoc}
  979. */
  980. public function deleteNodes(array $operations)
  981. {
  982. $this->assertLoggedIn();
  983. foreach ($operations as $op) {
  984. $this->deleteNode($op->srcPath);
  985. }
  986. return true;
  987. }
  988. /**
  989. * {@inheritDoc}
  990. */
  991. public function deleteNodeImmediately($path)
  992. {
  993. $this->prepareSave();
  994. $this->deleteNode($path);
  995. $this->finishSave();
  996. return true;
  997. }
  998. /**
  999. * TODO instead of calling the deletes separately, we should batch the delete query
  1000. * but careful with the caching!
  1001. *
  1002. * @param string $path node path to delete
  1003. *
  1004. */
  1005. protected function deleteNode($path)
  1006. {
  1007. if ('/' == $path) {
  1008. throw new ConstraintViolationException('You can not delete the root node of a repository');
  1009. }
  1010. $nodeId = $this->pathExists($path);
  1011. if (!$nodeId) {
  1012. throw new ItemNotFoundException("No node found at ".$path);
  1013. }
  1014. $params = array($path, $path."/%", $this->workspaceName);
  1015. // TODO on RDBMS that support deferred FKs we could skip this step
  1016. $query = 'SELECT id, path FROM phpcr_nodes WHERE (path = ? OR path LIKE ?) AND workspace_name = ?';
  1017. $stmt = $this->conn->executeQuery($query, $params);
  1018. $this->referencesToDelete += $stmt->fetchAll(\PDO::FETCH_UNIQUE | \PDO::FETCH_COLUMN);
  1019. try {
  1020. $query = 'DELETE FROM phpcr_nodes WHERE (path = ? OR path LIKE ?) AND workspace_name = ?';
  1021. $this->conn->executeUpdate($query, $params);
  1022. } catch (DBALException $e) {
  1023. throw new RepositoryException('Unexpected exception while deleting node ' . $path, $e->getCode(), $e);
  1024. }
  1025. }
  1026. /**
  1027. * {@inheritDoc}
  1028. */
  1029. public function deleteProperties(array $operations)
  1030. {
  1031. $this->assertLoggedIn();
  1032. foreach ($operations as $op) {
  1033. $this->deleteProperty($op->srcPath);
  1034. }
  1035. return true;
  1036. }
  1037. /**
  1038. * {@inheritDoc}
  1039. */
  1040. public function deletePropertyImmediately($path)
  1041. {
  1042. $this->prepareSave();
  1043. $this->deleteProperty($path);
  1044. $this->finishSave();
  1045. return true;
  1046. }
  1047. /**
  1048. * {@inheritDoc}
  1049. */
  1050. protected function deleteProperty($path)
  1051. {
  1052. $this->assertLoggedIn();
  1053. $nodePath = PathHelper::getParentPath($path);
  1054. $nodeId = $this->pathExists($nodePath);
  1055. if (!$nodeId) {
  1056. // no we really don't know that path
  1057. throw new ItemNotFoundException("No item found at ".$path);
  1058. }
  1059. $query = 'SELECT props FROM phpcr_nodes WHERE id = ?';
  1060. $xml = $this->conn->fetchColumn($query, array($nodeId));
  1061. $dom = new \DOMDocument('1.0', 'UTF-8');
  1062. $dom->loadXml($xml);
  1063. $found = false;
  1064. $propertyName = PathHelper::getNodeName($path);
  1065. foreach ($dom->getElementsByTagNameNS('http://www.jcp.org/jcr/sv/1.0', 'property') as $propertyNode) {
  1066. if ($propertyName == $propertyNode->getAttribute('sv:name')) {
  1067. $found = true;
  1068. // would be nice to have the property object to ask for type
  1069. // but its in state deleted, would mean lots of refactoring
  1070. if ($propertyNode->hasAttribute('sv:type')) {
  1071. $type = strtolower($propertyNode->getAttribute('sv:type'));
  1072. if (in_array($type, array('reference', 'weakreference'))) {
  1073. $table = $this->referenceTables['reference' === $type ? PropertyType::REFERENCE : PropertyType::WEAKREFERENCE];
  1074. try {
  1075. $query = "DELETE FROM $table WHERE source_id = ? AND source_property_name = ?";
  1076. $this->conn->executeUpdate($query, array($nodeId, $propertyName));
  1077. } catch (DBALException $e) {
  1078. throw new RepositoryException('Unexpected exception while cleaning up deleted nodes', $e->getCode(), $e);
  1079. }
  1080. }
  1081. }
  1082. $propertyNode->parentNode->removeChild($propertyNode);
  1083. break;
  1084. }
  1085. }
  1086. if (! $found) {
  1087. throw new ItemNotFoundException("Node $nodePath has no property $propertyName");
  1088. }
  1089. $xml = $dom->saveXML();
  1090. $query = 'UPDATE phpcr_nodes SET props = ? WHERE id = ?';
  1091. $params = array($xml, $nodeId);
  1092. try {
  1093. $this->conn->executeUpdate($query, $params);
  1094. } catch (DBALException $e) {
  1095. throw new RepositoryException("Unexpected exception while updating properties of $path", $e->getCode(), $e);
  1096. }
  1097. }
  1098. /**
  1099. * {@inheritDoc}
  1100. */
  1101. public function moveNodes(array $operations)
  1102. {
  1103. /** @var $op MoveNodeOperation */
  1104. foreach ($operations as $op) {
  1105. $this->moveNode($op->srcPath, $op->dstPath);
  1106. }
  1107. return true;
  1108. }
  1109. /**
  1110. * {@inheritDoc}
  1111. */
  1112. public function moveNodeImmediately($srcAbsPath, $dstAbspath)
  1113. {
  1114. $this->prepareSave();
  1115. $this->moveNode($srcAbsPath, $dstAbspath);
  1116. $this->finishSave();
  1117. return true;
  1118. }
  1119. /**
  1120. * Execute moving a single node
  1121. */
  1122. protected function moveNode($srcAbsPath, $dstAbsPath)
  1123. {
  1124. $this->assertLoggedIn();
  1125. PathHelper::assertValidAbsolutePath($dstAbsPath, true);
  1126. $srcNodeId = $this->pathExists($srcAbsPath);
  1127. if (!$srcNodeId) {
  1128. throw new PathNotFoundException("Source path '$srcAbsPath' not found");
  1129. }
  1130. if ($this->pathExists($dstAbsPath)) {
  1131. throw new ItemExistsException("Cannot move '$srcAbsPath' to '$dstAbsPath' because destination node already exists.");
  1132. }
  1133. if (!$this->pathExists(PathHelper::getParentPath($dstAbsPath))) {
  1134. throw new PathNotFoundException("Parent of the destination path '" . $dstAbsPath . "' has to exist.");
  1135. }
  1136. $query = 'SELECT path, id FROM phpcr_nodes WHERE path LIKE ? OR path = ? AND workspace_name = ? '
  1137. . $this->conn->getDatabasePlatform()->getForUpdateSQL();
  1138. $stmt = $this->conn->executeQuery($query, array($srcAbsPath . '/%', $srcAbsPath, $this->workspaceName));
  1139. /*
  1140. * TODO: https://github.com/jackalope/jackalope-doctrine-dbal/pull/26/files#L0R1057
  1141. * the other thing i wonder: can't you do the replacement inside sql instead of loading and then storing
  1142. * the node? this will be extremely slow for a large set of nodes. i think you should use query builder here
  1143. * rather than raw sql, to make it work on a maximum of platforms.
  1144. *
  1145. * can you try to do this please? if we don't figure out how to do it, at least fix the where criteria, and
  1146. * we can ask the doctrine community how to do the substring operation.
  1147. * http://stackoverflow.com/questions/8619421/correct-syntax-for-d…

Large files files are truncated, but you can click here to view the full file