PageRenderTime 53ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

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

https://github.com/paschke/jackalope-doctrine-dbal
PHP | 1922 lines | 1334 code | 262 blank | 326 comment | 162 complexity | f5930528a7fd15d18d554d710ed84d15 MD5 | raw file
Possible License(s): Apache-2.0
  1. <?php
  2. namespace Jackalope\Transport\DoctrineDBAL;
  3. use PHPCR\PropertyType;
  4. use PHPCR\Query\QOM\QueryObjectModelInterface;
  5. use PHPCR\Query\QOM\SelectorInterface;
  6. use PHPCR\Query\QueryInterface;
  7. use PHPCR\RepositoryException;
  8. use PHPCR\NamespaceException;
  9. use PHPCR\NamespaceRegistryInterface;
  10. use PHPCR\RepositoryInterface;
  11. use PHPCR\Util\UUIDHelper;
  12. use PHPCR\Util\QOM\Sql2ToQomQueryConverter;
  13. use PHPCR\NoSuchWorkspaceException;
  14. use PHPCR\ItemExistsException;
  15. use PHPCR\ItemNotFoundException;
  16. use PHPCR\ReferentialIntegrityException;
  17. use PHPCR\ValueFormatException;
  18. use PHPCR\PathNotFoundException;
  19. use PHPCR\Query\InvalidQueryException;
  20. use PHPCR\NodeType\ConstraintViolationException;
  21. use Doctrine\DBAL\Connection;
  22. use Doctrine\DBAL\Driver\PDOConnection;
  23. use Doctrine\DBAL\Platforms\MySqlPlatform;
  24. use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
  25. use Doctrine\DBAL\Platforms\SqlitePlatform;
  26. use Doctrine\DBAL\DBALException;
  27. use Jackalope\Node;
  28. use Jackalope\Property;
  29. use Jackalope\Query\Query;
  30. use Jackalope\Transport\BaseTransport;
  31. use Jackalope\Transport\QueryInterface as QueryTransport;
  32. use Jackalope\Transport\WritingInterface;
  33. use Jackalope\Transport\WorkspaceManagementInterface;
  34. use Jackalope\Transport\NodeTypeManagementInterface;
  35. use Jackalope\Transport\TransactionInterface;
  36. use Jackalope\Transport\StandardNodeTypes;
  37. use Jackalope\Transport\DoctrineDBAL\Query\QOMWalker;
  38. use Jackalope\NodeType\NodeTypeManager;
  39. use Jackalope\NodeType\NodeType;
  40. use Jackalope\NotImplementedException;
  41. use Jackalope\FactoryInterface;
  42. /**
  43. * Class to handle the communication between Jackalope and RDBMS via Doctrine DBAL.
  44. *
  45. * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License Version 2.0, January 2004
  46. *
  47. * @author Benjamin Eberlei <kontakt@beberlei.de>
  48. * @author Lukas Kahwe Smith <smith@pooteeweet.org>
  49. */
  50. class Client extends BaseTransport implements QueryTransport, WritingInterface, WorkspaceManagementInterface, NodeTypeManagementInterface, TransactionInterface
  51. {
  52. /**
  53. * @var Doctrine\DBAL\Connection
  54. */
  55. private $conn;
  56. /**
  57. * @var bool
  58. */
  59. private $loggedIn = false;
  60. /**
  61. * @var \PHPCR\SimpleCredentials
  62. */
  63. private $credentials;
  64. /**
  65. * @var string
  66. */
  67. protected $workspaceName;
  68. /**
  69. * @var array
  70. */
  71. private $nodeIdentifiers = array();
  72. /**
  73. * @var NodeTypeManager
  74. */
  75. private $nodeTypeManager;
  76. /**
  77. * @var bool
  78. */
  79. protected $inTransaction = false;
  80. /**
  81. * Check if an initial request on login should be send to check if repository exists
  82. * This is according to the JCR specifications and set to true by default
  83. * @see setCheckLoginOnServer
  84. * @var bool
  85. */
  86. private $checkLoginOnServer = true;
  87. /**
  88. * @var array
  89. */
  90. protected $namespaces = array();
  91. /**
  92. * @var string|null
  93. */
  94. private $sequenceWorkspaceName;
  95. /**
  96. * @var string|null
  97. */
  98. private $sequenceNodeName;
  99. /**
  100. * @var string|null
  101. */
  102. private $sequenceTypeName;
  103. public function __construct(FactoryInterface $factory, Connection $conn)
  104. {
  105. $this->factory = $factory;
  106. $this->conn = $conn;
  107. if ($conn->getDatabasePlatform() instanceof PostgreSqlPlatform) {
  108. $this->sequenceWorkspaceName = 'phpcr_workspaces_id_seq';
  109. $this->sequenceNodeName = 'phpcr_nodes_id_seq';
  110. $this->sequenceTypeName = 'phpcr_type_nodes_node_type_id_seq';
  111. }
  112. // @TODO: move to "SqlitePlatform" and rename to "registerExtraFunctions"?
  113. if ($this->conn->getDatabasePlatform() instanceof SqlitePlatform) {
  114. $this->registerSqliteFunctions($this->conn->getWrappedConnection());
  115. }
  116. }
  117. /**
  118. * @TODO: move to "SqlitePlatform" and rename to "registerExtraFunctions"?
  119. *
  120. * @param PDOConnection $sqliteConnection
  121. *
  122. * @return Client
  123. */
  124. private function registerSqliteFunctions(PDOConnection $sqliteConnection)
  125. {
  126. $sqliteConnection->sqliteCreateFunction('EXTRACTVALUE', function ($string, $expression) {
  127. $dom = new \DOMDocument('1.0', 'UTF-8');
  128. $dom->loadXML($string);
  129. $xpath = new \DOMXPath($dom);
  130. $list = $xpath->evaluate($expression);
  131. if (!is_object($list)) {
  132. return $list;
  133. }
  134. // @TODO: don't know if there are expressions returning more then one row
  135. if ($list->length > 0) {
  136. return $list->item(0)->textContent;
  137. }
  138. // @TODO: don't know if return value is right
  139. return null;
  140. }, 2);
  141. $sqliteConnection->sqliteCreateFunction('CONCAT', function () {
  142. return implode('', func_get_args());
  143. });
  144. return $this;
  145. }
  146. /**
  147. * @return Doctrine\DBAL\Connection
  148. */
  149. public function getConnection()
  150. {
  151. return $this->conn;
  152. }
  153. /**
  154. * {@inheritDoc}
  155. *
  156. */
  157. public function createWorkspace($name, $srcWorkspace = null)
  158. {
  159. if (null !== $srcWorkspace) {
  160. throw new NotImplementedException();
  161. }
  162. try {
  163. $this->conn->insert('phpcr_workspaces', array('name' => $name));
  164. } catch (\Exception $e) {
  165. throw new RepositoryException("Workspace '$name' already exists");
  166. }
  167. $this->conn->insert('phpcr_nodes', array(
  168. 'path' => '/',
  169. 'parent' => '',
  170. 'workspace_name'=> $name,
  171. 'identifier' => UUIDHelper::generateUUID(),
  172. 'type' => 'nt:unstructured',
  173. 'local_name' => '',
  174. 'namespace' => '',
  175. 'props' => '<?xml version="1.0" encoding="UTF-8"?>
  176. <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" />',
  177. // TODO compute proper value
  178. 'depth' => 0,
  179. ));
  180. }
  181. /**
  182. * {@inheritDoc}
  183. */
  184. public function login(\PHPCR\CredentialsInterface $credentials = null, $workspaceName = 'default')
  185. {
  186. $this->credentials = $credentials;
  187. $this->workspaceName = $workspaceName;
  188. if (!$this->checkLoginOnServer) {
  189. return true;
  190. }
  191. if (!$this->workspaceExists($workspaceName)) {
  192. if ('default' !== $workspaceName) {
  193. throw new NoSuchWorkspaceException("Requested workspace: $workspaceName");
  194. }
  195. // create default workspace if it not exists
  196. $this->createWorkspace($workspaceName);
  197. }
  198. $this->loggedIn = true;
  199. return true;
  200. }
  201. /**
  202. * {@inheritDoc}
  203. */
  204. public function logout()
  205. {
  206. if ($this->loggedIn) {
  207. $this->loggedIn = false;
  208. $this->conn->close();
  209. $this->conn = null;
  210. }
  211. }
  212. /**
  213. * {@inheritDoc}
  214. */
  215. public function setCheckLoginOnServer($bool)
  216. {
  217. $this->checkLoginOnServer = $bool;
  218. }
  219. protected function workspaceExists($workspaceName)
  220. {
  221. try {
  222. $query = 'SELECT 1 FROM phpcr_workspaces WHERE name = ?';
  223. $result = $this->conn->fetchColumn($query, array($workspaceName));
  224. } catch (\Exception $e) {
  225. if ($e instanceof DBALException || $e instanceof \PDOException) {
  226. if (1045 == $e->getCode()) {
  227. throw new \PHPCR\LoginException('Access denied with your credentials: '.$e->getMessage());
  228. }
  229. if ('42S02' == $e->getCode()) {
  230. throw new \PHPCR\RepositoryException('You did not properly set up the database for the repository. See README.md for more information. Message from backend: '.$e->getMessage());
  231. }
  232. throw new \PHPCR\RepositoryException('Unexpected error talking to the backend: '.$e->getMessage());
  233. }
  234. throw $e;
  235. }
  236. return $result;
  237. }
  238. protected function assertLoggedIn()
  239. {
  240. if (!$this->loggedIn) {
  241. if (!$this->checkLoginOnServer && $this->workspaceName) {
  242. $this->checkLoginOnServer = true;
  243. if ($this->login($this->credentials, $this->workspaceName)) {
  244. return;
  245. }
  246. }
  247. throw new RepositoryException('You need to be logged in for this operation');
  248. }
  249. }
  250. /**
  251. * {@inheritDoc}
  252. */
  253. public function getRepositoryDescriptors()
  254. {
  255. return array(
  256. RepositoryInterface::IDENTIFIER_STABILITY => RepositoryInterface::IDENTIFIER_STABILITY_INDEFINITE_DURATION,
  257. RepositoryInterface::REP_NAME_DESC => 'jackalope_doctrine_dbal',
  258. RepositoryInterface::REP_VENDOR_DESC => 'Jackalope Community',
  259. RepositoryInterface::REP_VENDOR_URL_DESC => 'http://github.com/jackalope',
  260. RepositoryInterface::REP_VERSION_DESC => '1.0.0-DEV',
  261. RepositoryInterface::SPEC_NAME_DESC => 'Content Repository for PHP',
  262. RepositoryInterface::SPEC_VERSION_DESC => '2.1',
  263. RepositoryInterface::NODE_TYPE_MANAGEMENT_AUTOCREATED_DEFINITIONS_SUPPORTED => true,
  264. RepositoryInterface::NODE_TYPE_MANAGEMENT_INHERITANCE => RepositoryInterface::NODE_TYPE_MANAGEMENT_INHERITANCE_SINGLE,
  265. RepositoryInterface::NODE_TYPE_MANAGEMENT_MULTIPLE_BINARY_PROPERTIES_SUPPORTED => true,
  266. RepositoryInterface::NODE_TYPE_MANAGEMENT_MULTIVALUED_PROPERTIES_SUPPORTED => true,
  267. RepositoryInterface::NODE_TYPE_MANAGEMENT_ORDERABLE_CHILD_NODES_SUPPORTED => true,
  268. RepositoryInterface::NODE_TYPE_MANAGEMENT_OVERRIDES_SUPPORTED => false,
  269. RepositoryInterface::NODE_TYPE_MANAGEMENT_PRIMARY_ITEM_NAME_SUPPORTED => true,
  270. RepositoryInterface::NODE_TYPE_MANAGEMENT_PROPERTY_TYPES => true,
  271. RepositoryInterface::NODE_TYPE_MANAGEMENT_RESIDUAL_DEFINITIONS_SUPPORTED => false,
  272. RepositoryInterface::NODE_TYPE_MANAGEMENT_SAME_NAME_SIBLINGS_SUPPORTED => false,
  273. RepositoryInterface::NODE_TYPE_MANAGEMENT_UPDATE_IN_USE_SUPPORTED => false,
  274. RepositoryInterface::NODE_TYPE_MANAGEMENT_VALUE_CONSTRAINTS_SUPPORTED => false,
  275. RepositoryInterface::OPTION_ACCESS_CONTROL_SUPPORTED => false,
  276. RepositoryInterface::OPTION_ACTIVITIES_SUPPORTED => false,
  277. RepositoryInterface::OPTION_BASELINES_SUPPORTED => false,
  278. RepositoryInterface::OPTION_JOURNALED_OBSERVATION_SUPPORTED => false,
  279. RepositoryInterface::OPTION_LIFECYCLE_SUPPORTED => false,
  280. RepositoryInterface::OPTION_LOCKING_SUPPORTED => false,
  281. RepositoryInterface::OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED => true,
  282. RepositoryInterface::OPTION_NODE_TYPE_MANAGEMENT_SUPPORTED => true,
  283. RepositoryInterface::OPTION_OBSERVATION_SUPPORTED => false,
  284. RepositoryInterface::OPTION_RETENTION_SUPPORTED => false,
  285. RepositoryInterface::OPTION_SHAREABLE_NODES_SUPPORTED => false,
  286. RepositoryInterface::OPTION_SIMPLE_VERSIONING_SUPPORTED => false,
  287. RepositoryInterface::OPTION_TRANSACTIONS_SUPPORTED => true,
  288. RepositoryInterface::OPTION_UNFILED_CONTENT_SUPPORTED => true,
  289. RepositoryInterface::OPTION_UPDATE_MIXIN_NODETYPES_SUPPORTED => true,
  290. RepositoryInterface::OPTION_UPDATE_PRIMARY_NODETYPE_SUPPORTED => true,
  291. RepositoryInterface::OPTION_VERSIONING_SUPPORTED => false,
  292. RepositoryInterface::OPTION_WORKSPACE_MANAGEMENT_SUPPORTED => true,
  293. RepositoryInterface::OPTION_XML_EXPORT_SUPPORTED => true,
  294. RepositoryInterface::OPTION_XML_IMPORT_SUPPORTED => true,
  295. RepositoryInterface::QUERY_FULL_TEXT_SEARCH_SUPPORTED => true,
  296. RepositoryInterface::QUERY_JOINS => RepositoryInterface::QUERY_JOINS_NONE,
  297. RepositoryInterface::QUERY_LANGUAGES => array(QueryInterface::JCR_SQL2, QueryInterface::JCR_JQOM),
  298. RepositoryInterface::QUERY_STORED_QUERIES_SUPPORTED => false,
  299. RepositoryInterface::WRITE_SUPPORTED => true,
  300. );
  301. }
  302. /**
  303. * {@inheritDoc}
  304. */
  305. public function getNamespaces()
  306. {
  307. if (empty($this->namespaces)) {
  308. $query = 'SELECT * FROM phpcr_namespaces';
  309. $data = $this->conn->fetchAll($query);
  310. $this->namespaces = array(
  311. NamespaceRegistryInterface::PREFIX_EMPTY => NamespaceRegistryInterface::NAMESPACE_EMPTY,
  312. NamespaceRegistryInterface::PREFIX_JCR => NamespaceRegistryInterface::NAMESPACE_JCR,
  313. NamespaceRegistryInterface::PREFIX_NT => NamespaceRegistryInterface::NAMESPACE_NT,
  314. NamespaceRegistryInterface::PREFIX_MIX => NamespaceRegistryInterface::NAMESPACE_MIX,
  315. NamespaceRegistryInterface::PREFIX_XML => NamespaceRegistryInterface::NAMESPACE_XML,
  316. NamespaceRegistryInterface::PREFIX_SV => NamespaceRegistryInterface::NAMESPACE_SV,
  317. 'phpcr' => 'http://github.com/jackalope/jackalope', // TODO: Namespace?
  318. );
  319. foreach ($data as $row) {
  320. $this->namespaces[$row['prefix']] = $row['uri'];
  321. }
  322. }
  323. return $this->namespaces;
  324. }
  325. /**
  326. * {@inheritDoc}
  327. */
  328. public function copyNode($srcAbsPath, $dstAbsPath, $srcWorkspace = null)
  329. {
  330. $this->assertLoggedIn();
  331. $workspaceName = $this->workspaceName;
  332. if (null !== $srcWorkspace) {
  333. if (!$this->workspaceExists($srcWorkspace)) {
  334. throw new NoSuchWorkspaceException("Source workspace '$srcWorkspace' does not exist.");
  335. }
  336. }
  337. $this->assertValidPath($dstAbsPath, true);
  338. $srcNodeId = $this->pathExists($srcAbsPath);
  339. if (!$srcNodeId) {
  340. throw new PathNotFoundException("Source path '$srcAbsPath' not found");
  341. }
  342. if ($this->pathExists($dstAbsPath)) {
  343. throw new ItemExistsException("Cannot copy to destination path '$dstAbsPath' that already exists.");
  344. }
  345. if (!$this->pathExists($this->getParentPath($dstAbsPath))) {
  346. throw new PathNotFoundException("Parent of the destination path '" . $this->getParentPath($dstAbsPath) . "' has to exist.");
  347. }
  348. // Algorithm:
  349. // 1. Select all nodes with path $srcAbsPath."%" and iterate them
  350. // 2. create a new node with path $dstAbsPath + leftovers, with a new uuid. Save old => new uuid
  351. // 3. copy all properties from old node to new node
  352. // 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.
  353. // 5. "May drop mixin types"
  354. $query = 'SELECT * FROM phpcr_nodes WHERE path LIKE ? AND workspace_name = ?';
  355. $stmt = $this->conn->executeQuery($query, array($srcAbsPath . '%', $workspaceName));
  356. foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
  357. $newPath = str_replace($srcAbsPath, $dstAbsPath, $row['path']);
  358. $dom = new \DOMDocument('1.0', 'UTF-8');
  359. $dom->loadXML($row['props']);
  360. $propsData = array('dom' => $dom, 'binaryData' => array());
  361. //when copying a node, it is always a new node, then $isNewNode is set to true
  362. $newNodeId = $this->syncNode(null, $newPath, $this->getParentPath($newPath), $row['type'], true, array(), $propsData);
  363. $query = 'INSERT INTO phpcr_binarydata (node_id, property_name, workspace_name, idx, data)'.
  364. ' SELECT ?, b.property_name, ?, b.idx, b.data FROM phpcr_binarydata b WHERE b.node_id = ?';
  365. $this->conn->executeUpdate($query, array($newNodeId, $this->workspaceName, $srcNodeId));
  366. }
  367. }
  368. /**
  369. * @param string $path
  370. * @return array
  371. */
  372. private function getJcrName($path)
  373. {
  374. $name = implode('', array_slice(explode('/', $path), -1, 1));
  375. if (strpos($name, ':') === false) {
  376. $alias = '';
  377. } else {
  378. list($alias, $name) = explode(':', $name);
  379. }
  380. $namespaces = $this->getNamespaces();
  381. if (!isset($namespaces[$alias])) {
  382. throw new NamespaceException('the namespace ' . $alias . ' was not registered.');
  383. }
  384. return array($namespaces[$alias], $name);
  385. }
  386. /**
  387. * Actually write the node into the database
  388. *
  389. * @param string $uuid node uuid
  390. * @param string $path absolute path of the node
  391. * @param string $parent absolute path of the parent node
  392. * @param string $type node type
  393. * @param bool $isNewNode new nodes to insert (true) or existing node to update (false)
  394. * @param array $props
  395. * @param array $propsData
  396. *
  397. * @return bool|mixed|string
  398. *
  399. * @throws \Exception|\PHPCR\ItemExistsException|\PHPCR\RepositoryException
  400. */
  401. private function syncNode($uuid, $path, $parent, $type, $isNewNode, $props = array(), $propsData = array())
  402. {
  403. // TODO: Not sure if there are always ALL props in $props, should we grab the online data here?
  404. // TODO: Binary data is handled very inefficiently here, UPSERT will really be necessary here as well as lazy handling
  405. if (!$propsData) {
  406. $propsData = $this->propsToXML($props);
  407. }
  408. if (null === $uuid) {
  409. $uuid = UUIDHelper::generateUUID();
  410. }
  411. if ($isNewNode) {
  412. list($namespace, $localName) = $this->getJcrName($path);
  413. $qb = $this->conn->createQueryBuilder();
  414. $qb->select(':identifier, :type, :path, :local_name, :namespace, :parent, :workspace_name, :props, :depth, COALESCE(MAX(n.sort_order), 0) + 1')
  415. ->from('phpcr_nodes', 'n')
  416. ->where('n.parent = :parent_a');
  417. $sql = $qb->getSql();
  418. try {
  419. $insert = "INSERT INTO phpcr_nodes (identifier, type, path, local_name, namespace, parent, workspace_name, props, depth, sort_order) " . $sql;
  420. $this->conn->executeUpdate($insert, array(
  421. 'identifier' => $uuid,
  422. 'type' => $type,
  423. 'path' => $path,
  424. 'local_name' => $localName,
  425. 'namespace' => $namespace,
  426. 'parent' => $parent,
  427. 'workspace_name' => $this->workspaceName,
  428. 'props' => $propsData['dom']->saveXML(),
  429. // TODO compute proper value
  430. 'depth' => 0,
  431. 'parent_a' => $parent,
  432. ));
  433. } catch (\PDOException $e) {
  434. throw new ItemExistsException('Item ' . $path . ' already exists in the database');
  435. } catch (DBALException $e) {
  436. throw new ItemExistsException('Item ' . $path . ' already exists in the database');
  437. }
  438. $nodeId = $this->conn->lastInsertId($this->sequenceNodeName);
  439. } else {
  440. $nodeId = $this->pathExists($path);
  441. if (!$nodeId) {
  442. throw new RepositoryException();
  443. }
  444. $this->conn->update('phpcr_nodes', array('props' => $propsData['dom']->saveXML()), array('id' => $nodeId));
  445. }
  446. $this->nodeIdentifiers[$path] = $uuid;
  447. if (isset($propsData['binaryData'])) {
  448. $this->syncBinaryData($nodeId, $propsData['binaryData']);
  449. }
  450. $this->syncForeignKeys($nodeId, $path, $props);
  451. return $nodeId;
  452. }
  453. private function syncBinaryData($nodeId, $binaryData)
  454. {
  455. foreach ($binaryData as $propertyName => $binaryValues) {
  456. foreach ($binaryValues as $idx => $data) {
  457. // TODO verify in which cases we can just update
  458. $params = array(
  459. 'node_id' => $nodeId,
  460. 'property_name' => $propertyName,
  461. 'workspace_name' => $this->workspaceName,
  462. );
  463. $this->conn->delete('phpcr_binarydata', $params);
  464. $params['idx'] = $idx;
  465. $params['data'] = $data;
  466. $types = array(
  467. \PDO::PARAM_INT,
  468. \PDO::PARAM_STR,
  469. \PDO::PARAM_STR,
  470. \PDO::PARAM_INT,
  471. \PDO::PARAM_LOB
  472. );
  473. $this->conn->insert('phpcr_binarydata', $params, $types);
  474. }
  475. }
  476. }
  477. private function syncForeignKeys($nodeId, $path, $props)
  478. {
  479. $this->conn->delete('phpcr_nodes_foreignkeys', array('source_id' => $nodeId));
  480. foreach ($props as $property) {
  481. $type = $property->getType();
  482. if (PropertyType::REFERENCE == $type || PropertyType::WEAKREFERENCE == $type) {
  483. $values = array_unique( $property->isMultiple() ? $property->getString() : array($property->getString()) );
  484. foreach ($values as $value) {
  485. try {
  486. $targetId = $this->pathExists(self::getNodePathForIdentifier($value));
  487. $this->conn->insert('phpcr_nodes_foreignkeys', array(
  488. 'source_id' => $nodeId,
  489. 'source_property_name' => $property->getName(),
  490. 'target_id' => $targetId,
  491. 'type' => $type
  492. ));
  493. } catch (ItemNotFoundException $e) {
  494. if (PropertyType::REFERENCE == $type) {
  495. throw new ReferentialIntegrityException(
  496. "Trying to store reference to non-existant node with path '$value' in node $path property " . $property->getName()
  497. );
  498. }
  499. }
  500. }
  501. }
  502. }
  503. }
  504. static public function xmlToProps($xml, $filter = null)
  505. {
  506. $props = array();
  507. $dom = new \DOMDocument('1.0', 'UTF-8');
  508. $dom->loadXML($xml);
  509. foreach ($dom->getElementsByTagNameNS('http://www.jcp.org/jcr/sv/1.0', 'property') as $propertyNode) {
  510. $name = $propertyNode->getAttribute('sv:name');
  511. $values = array();
  512. $type = PropertyType::valueFromName($propertyNode->getAttribute('sv:type'));
  513. foreach ($propertyNode->childNodes as $valueNode) {
  514. switch ($type) {
  515. case PropertyType::NAME:
  516. case PropertyType::URI:
  517. case PropertyType::WEAKREFERENCE:
  518. case PropertyType::REFERENCE:
  519. case PropertyType::PATH:
  520. case PropertyType::DECIMAL:
  521. case PropertyType::STRING:
  522. $values[] = $valueNode->nodeValue;
  523. break;
  524. case PropertyType::BOOLEAN:
  525. $values[] = (bool)$valueNode->nodeValue;
  526. break;
  527. case PropertyType::LONG:
  528. $values[] = (int)$valueNode->nodeValue;
  529. break;
  530. case PropertyType::BINARY:
  531. $values[] = (int)$valueNode->nodeValue;
  532. break;
  533. case PropertyType::DATE:
  534. $values[] = $valueNode->nodeValue;
  535. break;
  536. case PropertyType::DOUBLE:
  537. $values[] = (double)$valueNode->nodeValue;
  538. break;
  539. default:
  540. throw new \InvalidArgumentException("Type with constant $type not found.");
  541. }
  542. }
  543. // only return the properties that pass through the filter callback
  544. if (null !== $filter && is_callable($filter)) {
  545. if (false === $filter($name, $values)) {
  546. continue;
  547. }
  548. }
  549. if (PropertyType::BINARY == $type) {
  550. if (1 == $propertyNode->getAttribute('sv:multi-valued')) {
  551. $props[':' . $name] = $values;
  552. } else {
  553. $props[':' . $name] = $values[0];
  554. }
  555. } else {
  556. if (1 == $propertyNode->getAttribute('sv:multi-valued')) {
  557. $props[$name] = $values;
  558. } else {
  559. $props[$name] = $values[0];
  560. }
  561. $props[':' . $name] = $type;
  562. }
  563. }
  564. return $props;
  565. }
  566. /**
  567. * Seperate properties array into an xml and binary data.
  568. *
  569. * @param array $properties
  570. * @param bool $inlineBinaries
  571. * @return array ('dom' => $dom, 'binary' => streams)
  572. */
  573. public function propsToXML($properties, $inlineBinaries = false)
  574. {
  575. $namespaces = array(
  576. 'mix' => "http://www.jcp.org/jcr/mix/1.0",
  577. 'nt' => "http://www.jcp.org/jcr/nt/1.0",
  578. 'xs' => "http://www.w3.org/2001/XMLSchema",
  579. 'jcr' => "http://www.jcp.org/jcr/1.0",
  580. 'sv' => "http://www.jcp.org/jcr/sv/1.0",
  581. 'rep' => "internal"
  582. );
  583. $dom = new \DOMDocument('1.0', 'UTF-8');
  584. $rootNode = $dom->createElement('sv:node');
  585. foreach ($namespaces as $namespace => $uri) {
  586. $rootNode->setAttribute('xmlns:' . $namespace, $uri);
  587. }
  588. $dom->appendChild($rootNode);
  589. $binaryData = null;
  590. foreach ($properties as $property) {
  591. /* @var $property Property */
  592. $propertyNode = $dom->createElement('sv:property');
  593. $propertyNode->setAttribute('sv:name', $property->getName());
  594. $propertyNode->setAttribute('sv:type', PropertyType::nameFromValue($property->getType()));
  595. $propertyNode->setAttribute('sv:multi-valued', $property->isMultiple() ? '1' : '0');
  596. switch ($property->getType()) {
  597. case PropertyType::NAME:
  598. case PropertyType::URI:
  599. case PropertyType::WEAKREFERENCE:
  600. case PropertyType::REFERENCE:
  601. case PropertyType::PATH:
  602. case PropertyType::STRING:
  603. $values = $property->getString();
  604. break;
  605. case PropertyType::DECIMAL:
  606. $values = $property->getDecimal();
  607. break;
  608. case PropertyType::BOOLEAN:
  609. $values = array_map('intval', (array) $property->getBoolean());
  610. break;
  611. case PropertyType::LONG:
  612. $values = $property->getLong();
  613. break;
  614. case PropertyType::BINARY:
  615. if ($property->isNew() || $property->isModified()) {
  616. if ($property->isMultiple()) {
  617. $values = array();
  618. foreach ($property->getValueForStorage() as $stream) {
  619. if (null === $stream) {
  620. $binary = '';
  621. } else {
  622. $binary = stream_get_contents($stream);
  623. fclose($stream);
  624. }
  625. $binaryData[$property->getName()][] = $binary;
  626. $values[] = strlen($binary);
  627. }
  628. } else {
  629. $stream = $property->getValueForStorage();
  630. if (null === $stream) {
  631. $binary = '';
  632. } else {
  633. $binary = stream_get_contents($stream);
  634. fclose($stream);
  635. }
  636. $binaryData[$property->getName()][] = $binary;
  637. $values = strlen($binary);
  638. }
  639. } else {
  640. $values = $property->getLength();
  641. if (!$property->isMultiple() && empty($values)) {
  642. // TODO: not sure why this happens.
  643. $values = array(0);
  644. }
  645. }
  646. break;
  647. case PropertyType::DATE:
  648. $date = $property->getDate();
  649. if (!$date instanceof \DateTime) {
  650. $date = new \DateTime("now");
  651. }
  652. $values = $date->format('r');
  653. break;
  654. case PropertyType::DOUBLE:
  655. $values = $property->getDouble();
  656. break;
  657. default:
  658. throw new RepositoryException('unknown type '.$property->getType());
  659. }
  660. foreach ((array)$values as $value) {
  661. $element = $propertyNode->appendChild($dom->createElement('sv:value'));
  662. $element->appendChild($dom->createTextNode($value));
  663. }
  664. $rootNode->appendChild($propertyNode);
  665. }
  666. return array('dom' => $dom, 'binaryData' => $binaryData);
  667. }
  668. /**
  669. * {@inheritDoc}
  670. */
  671. public function getAccessibleWorkspaceNames()
  672. {
  673. $query = "SELECT DISTINCT name FROM phpcr_workspaces";
  674. $stmt = $this->conn->executeQuery($query);
  675. return $stmt->fetchAll(\PDO::FETCH_COLUMN);
  676. }
  677. /**
  678. * {@inheritDoc}
  679. */
  680. public function getNode($path)
  681. {
  682. $this->assertValidPath($path);
  683. $this->assertLoggedIn();
  684. $query = 'SELECT * FROM phpcr_nodes WHERE path = ? AND workspace_name = ?';
  685. $row = $this->conn->fetchAssoc($query, array($path, $this->workspaceName));
  686. if (!$row) {
  687. throw new ItemNotFoundException("Item $path not found in workspace ".$this->workspaceName);
  688. }
  689. return $this->getNodeData($path, $row);
  690. }
  691. private function getNodeData($path, $row)
  692. {
  693. $data = new \stdClass();
  694. $data->{'jcr:primaryType'} = $row['type'];
  695. $this->nodeIdentifiers[$path] = $row['identifier'];
  696. $query = 'SELECT path FROM phpcr_nodes WHERE parent = ? AND workspace_name = ? ORDER BY sort_order ASC';
  697. $children = $this->conn->fetchAll($query, array($path, $this->workspaceName));
  698. foreach ($children as $child) {
  699. $childName = explode('/', $child['path']);
  700. $childName = end($childName);
  701. $data->{$childName} = new \stdClass();
  702. }
  703. $dom = new \DOMDocument('1.0', 'UTF-8');
  704. $dom->loadXML($row['props']);
  705. foreach ($dom->getElementsByTagNameNS('http://www.jcp.org/jcr/sv/1.0', 'property') as $propertyNode) {
  706. $name = $propertyNode->getAttribute('sv:name');
  707. $values = array();
  708. $type = PropertyType::valueFromName($propertyNode->getAttribute('sv:type'));
  709. foreach ($propertyNode->childNodes as $valueNode) {
  710. switch ($type) {
  711. case PropertyType::NAME:
  712. case PropertyType::URI:
  713. case PropertyType::WEAKREFERENCE:
  714. case PropertyType::REFERENCE:
  715. case PropertyType::PATH:
  716. case PropertyType::DECIMAL:
  717. case PropertyType::STRING:
  718. $values[] = $valueNode->nodeValue;
  719. break;
  720. case PropertyType::BOOLEAN:
  721. $values[] = (bool)$valueNode->nodeValue;
  722. break;
  723. case PropertyType::LONG:
  724. $values[] = (int)$valueNode->nodeValue;
  725. break;
  726. case PropertyType::BINARY:
  727. $values[] = (int)$valueNode->nodeValue;
  728. break;
  729. case PropertyType::DATE:
  730. $values[] = $valueNode->nodeValue;
  731. break;
  732. case PropertyType::DOUBLE:
  733. $values[] = (double)$valueNode->nodeValue;
  734. break;
  735. default:
  736. throw new \InvalidArgumentException("Type with constant " . $type . " not found.");
  737. }
  738. }
  739. if (PropertyType::BINARY == $type) {
  740. if (1 == $propertyNode->getAttribute('sv:multi-valued')) {
  741. $data->{':' . $name} = $values;
  742. } else {
  743. $data->{':' . $name} = $values[0];
  744. }
  745. } else {
  746. if (1 == $propertyNode->getAttribute('sv:multi-valued')) {
  747. $data->{$name} = $values;
  748. } else {
  749. $data->{$name} = $values[0];
  750. }
  751. $data->{':' . $name} = $type;
  752. }
  753. }
  754. // If the node is referenceable, return jcr:uuid.
  755. $is_referenceable = false;
  756. if (isset($data->{"jcr:mixinTypes"})) {
  757. foreach ((array) $data->{"jcr:mixinTypes"} as $mixin) {
  758. if ($this->nodeTypeManager->getNodeType($mixin)->isNodeType('mix:referenceable')) {
  759. $is_referenceable = true;
  760. break;
  761. }
  762. }
  763. }
  764. if ($is_referenceable) {
  765. $data->{'jcr:uuid'} = $row['identifier'];
  766. }
  767. return $data;
  768. }
  769. /**
  770. * {@inheritDoc}
  771. */
  772. public function getNodes($paths)
  773. {
  774. foreach ($paths as $path) {
  775. $this->assertValidPath($path);
  776. }
  777. $this->assertLoggedIn();
  778. $query = 'SELECT path AS arraykey, id, path, parent, local_name, namespace, workspace_name, identifier, type, props, depth, sort_order
  779. FROM phpcr_nodes WHERE workspace_name = ? AND path IN (?)';
  780. $params = array($this->workspaceName, $paths);
  781. $stmt = $this->conn->executeQuery($query, $params, array(\PDO::PARAM_STR, Connection::PARAM_STR_ARRAY));
  782. $all = $stmt->fetchAll(\PDO::FETCH_UNIQUE | \PDO::FETCH_GROUP);
  783. $nodes = array();
  784. foreach ($paths as $key => $path) {
  785. if (isset($all[$path])) {
  786. $nodes[$key] = $this->getNodeData($path, $all[$path]);
  787. }
  788. }
  789. return $nodes;
  790. }
  791. private function pathExists($path)
  792. {
  793. $query = 'SELECT id FROM phpcr_nodes WHERE path = ? AND workspace_name = ?';
  794. if ($nodeId = $this->conn->fetchColumn($query, array($path, $this->workspaceName))) {
  795. return $nodeId;
  796. }
  797. return false;
  798. }
  799. /**
  800. * {@inheritDoc}
  801. */
  802. public function deleteNode($path)
  803. {
  804. $this->assertLoggedIn();
  805. if ('/' == $path) {
  806. throw new ConstraintViolationException('You can not delete the root node of a repository');
  807. }
  808. $nodeId = $this->pathExists($path);
  809. if (!$nodeId) {
  810. throw new ItemNotFoundException("No node found at ".$path);
  811. }
  812. $params = array($path, $path."/%", $this->workspaceName);
  813. $query =
  814. 'SELECT COUNT(*)
  815. FROM phpcr_nodes_foreignkeys fk
  816. INNER JOIN phpcr_nodes n ON n.id = fk.target_id
  817. WHERE (n.path = ? OR n.path LIKE ?)
  818. AND workspace_name = ?
  819. AND fk.type = ' . PropertyType::REFERENCE;
  820. $fkReferences = $this->conn->fetchColumn($query, $params);
  821. if ($fkReferences > 0) {
  822. /*
  823. TODO: if we had logging, we could report which nodes
  824. $query =
  825. 'SELECT fk.source_id
  826. FROM phpcr_nodes_foreignkeys fk
  827. INNER JOIN phpcr_nodes n ON n.id = fk.target_id
  828. INNER JOIN phpcr_nodes f ON f.id = fk.source_id
  829. WHERE (n.path = ? OR n.path LIKE ?)
  830. AND n.workspace_name = ?
  831. AND fk.type = ' . PropertyType::REFERENCE;
  832. $paths = $this->conn->fetchAssoc($query, $params);
  833. */
  834. throw new ReferentialIntegrityException("Cannot delete $path: A reference points to this node or a subnode");
  835. }
  836. $query =
  837. 'DELETE FROM phpcr_nodes
  838. WHERE (path = ? OR path LIKE ?)
  839. AND workspace_name = ?';
  840. $this->conn->executeUpdate($query, $params);
  841. }
  842. /**
  843. * {@inheritDoc}
  844. */
  845. public function deleteProperty($path)
  846. {
  847. $this->assertLoggedIn();
  848. $nodePath = $this->getParentPath($path);
  849. $nodeId = $this->pathExists($nodePath);
  850. if (!$nodeId) {
  851. // no we really don't know that path
  852. throw new ItemNotFoundException("No item found at ".$path);
  853. }
  854. if ('/' == $nodePath) {
  855. // root node is a special case
  856. $propertyName = substr($path, 1);
  857. } else {
  858. $propertyName = str_replace($nodePath . '/', '', $path);
  859. }
  860. $query = 'SELECT props FROM phpcr_nodes WHERE id = ?';
  861. $xml = $this->conn->fetchColumn($query, array($nodeId));
  862. $dom = new \DOMDocument('1.0', 'UTF-8');
  863. $dom->loadXml($xml);
  864. $found = false;
  865. foreach ($dom->getElementsByTagNameNS('http://www.jcp.org/jcr/sv/1.0', 'property') as $propertyNode) {
  866. if ($propertyName == $propertyNode->getAttribute('sv:name')) {
  867. $found = true;
  868. // would be nice to have the property object to ask for type
  869. // but its in state deleted, would mean lots of refactoring
  870. if ($propertyNode->hasAttribute('sv:type') &&
  871. ('reference' == $propertyNode->getAttribute('sv:type')
  872. || 'weakreference' == $propertyNode->getAttribute('sv:type')
  873. )
  874. ) {
  875. $query = 'DELETE FROM phpcr_nodes_foreignkeys
  876. WHERE source_id = ?
  877. AND source_property_name = ?';
  878. $this->conn->executeUpdate($query, array($nodeId, $propertyName));
  879. }
  880. $propertyNode->parentNode->removeChild($propertyNode);
  881. break;
  882. }
  883. }
  884. if (! $found) {
  885. throw new ItemNotFoundException("Node $nodePath has no property $propertyName");
  886. }
  887. $xml = $dom->saveXML();
  888. $query = 'UPDATE phpcr_nodes SET props = ? WHERE id = ?';
  889. $params = array($xml, $nodeId);
  890. $this->conn->executeUpdate($query, $params);
  891. }
  892. /**
  893. * {@inheritDoc}
  894. */
  895. public function moveNode($srcAbsPath, $dstAbsPath)
  896. {
  897. $this->assertLoggedIn();
  898. $this->assertValidPath($dstAbsPath, true);
  899. $srcNodeId = $this->pathExists($srcAbsPath);
  900. if (!$srcNodeId) {
  901. throw new PathNotFoundException("Source path '$srcAbsPath' not found");
  902. }
  903. if ($this->pathExists($dstAbsPath)) {
  904. throw new ItemExistsException("Cannot move '$srcAbsPath' to '$dstAbsPath' because destination node already exists.");
  905. }
  906. if (!$this->pathExists($this->getParentPath($dstAbsPath))) {
  907. throw new PathNotFoundException("Parent of the destination path '" . $this->getParentPath($dstAbsPath) . "' has to exist.");
  908. }
  909. $query = 'SELECT path, id FROM phpcr_nodes WHERE path LIKE ? OR path = ? AND workspace_name = ? ' . $this->conn->getDatabasePlatform()->getForUpdateSQL();
  910. $stmt = $this->conn->executeQuery($query, array($srcAbsPath . '/%', $srcAbsPath, $this->workspaceName));
  911. /*
  912. * TODO: https://github.com/jackalope/jackalope-doctrine-dbal/pull/26/files#L0R1057
  913. * the other thing i wonder: can't you do the replacement inside sql instead of loading and then storing
  914. * the node? this will be extremly slow for a large set of nodes. i think you should use query builder here
  915. * rather than raw sql, to make it work on a maximum of platforms.
  916. *
  917. * can you try to do this please? if we don't figure out how to do it, at least fix the where criteria, and
  918. * we can ask the doctrine community how to do the substring operation.
  919. * http://stackoverflow.com/questions/8619421/correct-syntax-for-doctrine2s-query-builder-substring-helper-method
  920. */
  921. $ids = '';
  922. $query = "UPDATE phpcr_nodes SET ";
  923. $updatePathCase = "path = CASE ";
  924. $updateParentCase = "parent = CASE ";
  925. $updateLocalNameCase = "local_name = CASE ";
  926. $updateSortOrderCase = "sort_order = CASE ";
  927. $i = 0;
  928. while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
  929. $values[':id' . $i] = $row['id'];
  930. $values[':path' . $i] = str_replace($srcAbsPath, $dstAbsPath, $row['path']);
  931. $values[':parent' . $i] = dirname($values[':path' . $i]);
  932. $updatePathCase .= "WHEN id = :id" . $i . " THEN :path" . $i . " ";
  933. $updateParentCase .= "WHEN id = :id" . $i . " THEN :parent" . $i . " ";
  934. if ($srcAbsPath === $row['path']) {
  935. $values[':localname' . $i] = basename($values[':path' . $i]);
  936. $updateLocalNameCase .= "WHEN id = :id" . $i . " THEN :localname" . $i . " ";
  937. $updateSortOrderCase .= "WHEN id = :id" . $i . " THEN (SELECT * FROM ( SELECT MAX(x.sort_order) + 1 FROM phpcr_nodes x WHERE x.parent = :parent" . $i . ") y) ";
  938. }
  939. $ids .= $row['id'] . ',';
  940. $i ++;
  941. }
  942. $ids = rtrim($ids, ',');
  943. $updateLocalNameCase .= "ELSE local_name END, ";
  944. $updateSortOrderCase .= "ELSE sort_order END ";
  945. $query .= $updatePathCase . "END, " . $updateParentCase . "END, " . $updateLocalNameCase . $updateSortOrderCase;
  946. $query .= "WHERE id IN (" . $ids . ")";
  947. $this->conn->executeUpdate($query, $values);
  948. }
  949. /**
  950. * {@inheritDoc}
  951. */
  952. public function reorderNodes($absPath, $reorders)
  953. {
  954. $this->assertLoggedIn();
  955. /* Solution:
  956. * - Determine the current order (from DB query).
  957. * - Use the $reorders to calculate the new order.
  958. * - Compare the old and new sequences to generate the required update statements.
  959. * We cant just use the $reorders to get UPDATE statements directly as even a simple single move, from being the
  960. * last sibling to being the first, could result in the need to update the sort_order of every sibling.
  961. */
  962. // Retrieve an array of siblings names in the original order.
  963. $qb = $this->conn->createQueryBuilder();
  964. $qb->select("CONCAT(n.namespace,(CASE namespace WHEN '' THEN '' ELSE ':' END), n.local_name)")
  965. ->from('phpcr_nodes', 'n')
  966. ->where('n.parent = :absPath')
  967. ->orderBy('n.sort_order', 'ASC');
  968. $query = $qb->getSql() . ' ' . $this->conn->getDatabasePlatform()->getForUpdateSQL();
  969. $stmnt = $this->conn->executeQuery($query, array('absPath' => $absPath));
  970. while ($row = $stmnt->fetchColumn()) {
  971. $original[] = $row;
  972. }
  973. // Flip to access via the name.
  974. $modified = array_flip($original);
  975. foreach ($reorders as $reorder) {
  976. if (null === $reorder[1]) {
  977. // Case: need to move node to the end of the array.
  978. // Remove from old position and append to end.
  979. unset($modified[$reorder[0]]);
  980. $modified = array_flip($modified);
  981. $modified[] = $reorder[0];
  982. // Resequence keys and flip back so we can access via name again.
  983. $modified = array_values($modified);
  984. $modified = array_flip($modified);
  985. } else {
  986. // Case: need to move node to before the specified target.
  987. // Remove from old position, resequence the keys and flip back so we can access by name again.
  988. unset($modified[$reorder[0]]);
  989. $modified = array_keys($modified);
  990. $modified = array_flip($modified);
  991. // Get target position and splice in.
  992. $targetPos = $modified[$reorder[1]];
  993. $modified = array_flip($modified);
  994. array_splice($modified, $targetPos, 0, $reorder[0]);
  995. $modified = array_flip($modified);
  996. }
  997. }
  998. $values[':absPath'] = $absPath;
  999. $sql = "UPDATE phpcr_nodes SET sort_order = CASE CONCAT(
  1000. namespace,
  1001. (CASE namespace WHEN '' THEN '' ELSE ':' END),
  1002. local_name
  1003. )";
  1004. $i = 0;
  1005. foreach ($modified as $name => $order) {
  1006. $values[':name' . $i] = $name;
  1007. $values[':order' . $i] = $order;
  1008. $sql .= " WHEN :name" . $i . " THEN :order" . $i;
  1009. $i++;
  1010. }
  1011. $sql .= " ELSE sort_order END WHERE parent = :absPath";
  1012. $this->conn->executeUpdate($sql, $values);
  1013. }
  1014. /**
  1015. * Get parent path of a path.
  1016. *
  1017. * @param string $path
  1018. * @return string
  1019. */
  1020. private function getParentPath($path)
  1021. {
  1022. $parent = implode('/', array_slice(explode('/', $path), 0, -1));
  1023. if (!$parent) {
  1024. return '/';
  1025. }
  1026. return $parent;
  1027. }
  1028. /**
  1029. * TODO: we should move that into the common Jackalope BaseTransport or as new method of NodeType
  1030. * it will be helpful for other implementations.
  1031. *
  1032. * Validate this node with the nodetype and generate not yet existing
  1033. * autogenerated properties as necessary.
  1034. *
  1035. * @param Node $node
  1036. * @param NodeType $def
  1037. */
  1038. private function validateNode(Node $node, NodeType $def)
  1039. {
  1040. foreach ($def->getDeclaredChildNodeDefinitions() as $childDef) {
  1041. /* @var $childDef \PHPCR\NodeType\NodeDefinitionInterface */
  1042. if (!$node->hasNode($childDef->getName())) {
  1043. if ('*' === $childDef->getName()) {
  1044. continue;
  1045. }
  1046. if ($childDef->isMandatory() && !$childDef->isAutoCreated()) {
  1047. throw new RepositoryException(
  1048. "Child " . $childDef->getName() . " is mandatory, but is not present while ".
  1049. "saving " . $def->getName() . " at " . $node->getPath()
  1050. );
  1051. }
  1052. if ($childDef->isAutoCreated()) {
  1053. throw new NotImplementedException("Auto-creation of child node '".$def->getName()."#".$childDef->getName()."' is not yet supported in DoctrineDBAL transport.");
  1054. }
  1055. }
  1056. }
  1057. foreach ($def->getDeclaredPropertyDefinitions() as $propertyDef) {
  1058. /* @var $propertyDef \PHPCR\NodeType\PropertyDefinitionInterface */
  1059. if ('*' == $propertyDef->getName()) {
  1060. continue;
  1061. }
  1062. if (!$node->hasProperty($propertyDef->getName())) {
  1063. if ($propertyDef->isMandatory() && !$propertyDef->isAutoCreated()) {
  1064. throw new RepositoryException(
  1065. "Property " . $propertyDef->getName() . " is mandatory, but is not present while ".
  1066. "saving " . $def->getName() . " at " . $node->getPath()
  1067. );
  1068. }
  1069. if ($propertyDef->isAutoCreated()) {
  1070. switch ($propertyDef->getName()) {
  1071. case 'jcr:uuid':
  1072. $value = UUIDHelper::generateUUID();
  1073. break;
  1074. case 'jcr:createdBy':
  1075. case 'jcr:lastModifiedBy':
  1076. $value = $this->credentials->getUserID();
  1077. break;
  1078. case 'jcr:created':
  1079. case 'jcr:lastModified':
  1080. $value = new \DateTime();
  1081. break;
  1082. case 'jcr:etag':
  1083. // TODO: http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.7.12.1%20mix:etag
  1084. $value = 'TODO: generate from binary properties of this node';
  1085. break;
  1086. default:
  1087. $defaultValues = $propertyDef->getDefaultValues();
  1088. if ($propertyDef->isMultiple()) {
  1089. $value = $defaultValues;
  1090. } elseif (isset($defaultValues[0])) {
  1091. $value = $defaultValues[0];
  1092. } else {
  1093. // When implementing versionable or activity, we need to handle more properties explicitly
  1094. throw new RepositoryException('No default value for autocreated property '.
  1095. $propertyDef->getName(). ' at '.$node->getPath());
  1096. }
  1097. }
  1098. $node->setProperty(
  1099. $propertyDef->getName(),
  1100. $value,
  1101. $propertyDef->getRequiredType()
  1102. );
  1103. }
  1104. }
  1105. }
  1106. foreach ($node->getProperties() as $property) {
  1107. $this->assertValidProperty($property);
  1108. }
  1109. }
  1110. private function getResponsibleNodeTypes(Node $node)
  1111. {
  1112. // This is very slow i believe :-(
  1113. $nodeDef = $node->getPrimaryNodeType();
  1114. $nodeTypes = $node->getMixinNodeTypes();
  1115. array_unshift($nodeTypes, $nodeDef);
  1116. return $nodeTypes;
  1117. }
  1118. /**
  1119. * Recursively store a node and its children to the given absolute path.
  1120. *
  1121. * Transport stores the node at its path, with all properties and all
  1122. * children.
  1123. *
  1124. * @param \Jackalope\Node $node the node to store
  1125. * @param bool $saveChildren false to store only the current node and not its children
  1126. *
  1127. * @return bool true on success
  1128. *
  1129. * @throws \PHPCR\RepositoryException if not logged in
  1130. */
  1131. public function storeNode(Node $node, $saveChildren = true)
  1132. {
  1133. $this->assertLoggedIn();
  1134. $nodeTypes = $this->getResponsibleNodeTypes($node);
  1135. foreach ($nodeTypes as $nodeType) {
  1136. /* @var $nodeType \PHPCR\NodeType\NodeTypeDefinitionInterface */
  1137. $this->validateNode($node, $nodeType);
  1138. }
  1139. $properties = $node->getProperties();
  1140. $path = $node->getPath();
  1141. if (isset($this->nodeIdentifiers[$path])) {
  1142. $nodeIdentifier = $this->nodeIdentifiers[$path];
  1143. } elseif (isset($properties['jcr:uuid'])) {
  1144. $nodeIdentifier = $properties['jcr:uuid']->getValue();
  1145. } else {
  1146. // we always generate a uuid, even for non-referenceable nodes that have no automatic uuid
  1147. $nodeIdentifier = UUIDHelper::generateUUID();
  1148. }
  1149. $type = isset($properties['jcr:primaryType']) ? $properties['jcr:primaryType']->getValue() : "nt:unstructured";
  1150. $this->syncNode($nodeIdentifier, $path, $this->getParentPath($path), $type, $node->isNew(), $properties);
  1151. if (!$saveChildren) {
  1152. return true;
  1153. }
  1154. foreach ($node as $child) {
  1155. /** @var $child Node */
  1156. if ($child->isNew()) {
  1157. // recursively call ourselves
  1158. $this->storeNode($child);
  1159. }
  1160. // else this is an existing node moved to this location
  1161. }
  1162. return true;
  1163. }
  1164. /**
  1165. * {@inheritDoc}
  1166. */
  1167. public function storeProperty(Property $property)
  1168. {
  1169. $this->assertLoggedIn();
  1170. $node = $property->getParent();
  1171. //do not store the children nodes, already taken into account previously with storeNode
  1172. $this->storeNode($node, false);
  1173. return true;
  1174. }
  1175. /**
  1176. * Validation if all the data is correct before writing it into the database.
  1177. *
  1178. * @param \PHPCR\PropertyInterface $property
  1179. *
  1180. * @throws \PHPCR\ValueFormatException
  1181. *
  1182. * @return void
  1183. */
  1184. private function assertValidProperty($property)
  1185. {
  1186. $type = $property->getType();
  1187. switch ($type) {
  1188. case PropertyType::NAME:
  1189. $values = $property->getValue();
  1190. if (!$property->isMultiple()) {
  1191. $values = array($values);
  1192. }
  1193. foreach ($values as $value) {
  1194. $pos = strpos($value, ':');
  1195. if (false !== $pos) {
  1196. $prefix = substr($value, 0, $pos);
  1197. $this->getNamespaces();
  1198. if (!isset($this->namespaces[$prefix])) {
  1199. throw new ValueFormatException("Invalid PHPCR NAME at '" . $property->getPath() . "': The namespace prefix " . $prefix . " does not exist.");
  1200. }
  1201. }
  1202. }
  1203. break;
  1204. case PropertyType::PATH:
  1205. $values = $property->getValue();
  1206. if (!$property->isMultiple()) {
  1207. $values = array($values);
  1208. }
  1209. foreach ($values as $value) {
  1210. if (!preg_match('(((/|..)?[-a-zA-Z0-9:_]+)+)', $value)) {
  1211. throw new ValueFormatException("Invalid PATH '$value' at '" . $property->getPath() ."': Segments are separated by / and allowed chars are -a-zA-Z0-9:_");
  1212. }
  1213. }
  1214. break;
  1215. case PropertyType::URI:
  1216. $values = $property->getValue();
  1217. if (!$property->isMultiple()) {
  1218. $values = array($values);
  1219. }
  1220. foreach ($values as $value) {
  1221. if (!preg_match(self::VALIDATE_URI_RFC3986, $value)) {
  1222. throw new ValueFormatException("Invalid URI '$value' at '" . $property->getPath() ."': Has to follow RFC 3986.");
  1223. }
  1224. }
  1225. break;
  1226. }
  1227. }
  1228. /**
  1229. * {@inheritDoc}
  1230. */
  1231. public function getNodePathForIdentifier($uuid)
  1232. {
  1233. $this->assertLoggedIn();
  1234. $path = $this->conn->fetchColumn("SELECT path FROM phpcr_nodes WHERE identifier = ? AND workspace_name = ?", array($uuid, $this->workspaceName));
  1235. if (!$path) {
  1236. throw new ItemNotFoundException("no item found with uuid ".$uuid);
  1237. }
  1238. return $path;
  1239. }
  1240. /**
  1241. * {@inheritDoc}
  1242. */
  1243. public function getNodeTypes($nodeTypes = array())
  1244. {
  1245. $standardTypes = array();
  1246. foreach (StandardNodeTypes::getNodeTypeData() as $nodeTypeData) {
  1247. $standardTypes[$nodeTypeData['name']] = $nodeTypeData;
  1248. }
  1249. $userTypes = $this->fetchUserNodeTypes();
  1250. if ($nodeTypes) {
  1251. $nodeTypes = array_flip($nodeTypes);
  1252. // TODO: check if user types can override standard types.
  1253. return array_values(array_intersect_key($standardTypes, $nodeTypes) + array_intersect_key($userTypes, $nodeTypes));
  1254. }
  1255. return array_values($standardTypes + $userTypes);
  1256. }
  1257. /**
  1258. * Fetch a user-defined node-type definition.
  1259. *
  1260. * @param string $name
  1261. * @return array
  1262. */
  1263. protected function fetchUserNodeTypes()
  1264. {
  1265. $result = array();
  1266. $query = "SELECT * FROM phpcr_type_nodes";
  1267. foreach ($this->conn->fetchAll($query) as $data) {
  1268. $name = $data['name'];
  1269. $result[$name] = array(
  1270. 'name' => $name,
  1271. 'isAbstract' => (bool)$data['is_abstract'],
  1272. 'isMixin' => (bool)($data['is_mixin']),
  1273. 'isQueryable' => (bool)$data['queryable'],
  1274. 'hasOrderableChildNodes' => (bool)$data['orderable_child_nodes'],
  1275. 'primaryItemName' => $data['primary_item'],
  1276. 'declaredSuperTypeNames' => array_filter(explode(' ', $data['supertypes'])),
  1277. 'declaredPropertyDefinitions' => array(),
  1278. 'declaredNodeDefinitions' => array(),
  1279. );
  1280. $query = 'SELECT * FROM phpcr_type_props WHERE node_type_id = ?';
  1281. $props = $this->conn->fetchAll($query, array($data['node_type_id']));
  1282. foreach ($props as $propertyData) {
  1283. $result[$name]['declaredPropertyDefinitions'][] = array(
  1284. 'declaringNodeType' => $data['name'],
  1285. 'name' => $propertyData['name'],
  1286. 'isAutoCreated' => (bool)$propertyData['auto_created'],
  1287. 'isMandatory' => (bool)$propertyData['mandatory'],
  1288. 'isProtected' => (bool)$propertyData['protected'],
  1289. 'onParentVersion' => $propertyData['on_parent_version'],
  1290. 'requiredType' => $propertyData['required_type'],
  1291. 'multiple' => (bool)$propertyData['multiple'],
  1292. 'isFulltextSearchable' => (bool)$propertyData['fulltext_searchable'],
  1293. 'isQueryOrderable' => (bool)$propertyData['query_orderable'],
  1294. 'queryOperators' => array (
  1295. 0 => 'jcr.operator.equal.to',
  1296. 1 => 'jcr.operator.not.equal.to',
  1297. 2 => 'jcr.operator.greater.than',
  1298. 3 => 'jcr.operator.greater.than.or.equal.to',
  1299. 4 => 'jcr.operator.less.than',
  1300. 5 => 'jcr.operator.less.than.or.equal.to',
  1301. 6 => 'jcr.operator.like',
  1302. ),
  1303. 'defaultValue' => array($propertyData['default_value']),
  1304. );
  1305. }
  1306. $query = 'SELECT * FROM phpcr_type_childs WHERE node_type_id = ?';
  1307. $childs = $this->conn->fetchAll($query, array($data['node_type_id']));
  1308. foreach ($childs as $childData) {
  1309. $result[$name]['declaredNodeDefinitions'][] = array(
  1310. 'declaringNodeType' => $data['name'],
  1311. 'name' => $childData['name'],
  1312. 'isAutoCreated' => (bool)$childData['auto_created'],
  1313. 'isMandatory' => (bool)$childData['mandatory'],
  1314. 'isProtected' => (bool)$childData['protected'],
  1315. 'onParentVersion' => $childData['on_parent_version'],
  1316. 'allowsSameNameSiblings' => false,
  1317. 'defaultPrimaryTypeName' => $childData['default_type'],
  1318. 'requiredPrimaryTypeNames' => array_filter(explode(" ", $childData['primary_types'])),
  1319. );
  1320. }
  1321. }
  1322. return $result;
  1323. }
  1324. /**
  1325. * {@inheritDoc}
  1326. */
  1327. public function registerNodeTypesCnd($cnd, $allowUpdate)
  1328. {
  1329. throw new NotImplementedException('Not implemented yet');
  1330. }
  1331. /**
  1332. * {@inheritDoc}
  1333. */
  1334. public function registerNodeTypes($types, $allowUpdate)
  1335. {
  1336. foreach ($types as $type) {
  1337. /* @var $type \Jackalope\NodeType\NodeTypeDefinition */
  1338. $this->conn->insert('phpcr_type_nodes', array(
  1339. 'name' => $type->getName(),
  1340. 'supertypes' => implode(' ', $type->getDeclaredSuperTypeNames()),
  1341. 'is_abstract' => $type->isAbstract() ? 1 : 0,
  1342. 'is_mixin' => $type->isMixin() ? 1 : 0,
  1343. 'queryable' => $type->isQueryable() ? 1 : 0,
  1344. 'orderable_child_nodes' => $type->hasOrderableChildNodes() ? 1 : 0,
  1345. 'primary_item' => $type->getPrimaryItemName(),
  1346. ));
  1347. $nodeTypeId = $this->conn->lastInsertId($this->sequenceTypeName);
  1348. if ($propDefs = $type->getDeclaredPropertyDefinitions()) {
  1349. foreach ($propDefs as $propertyDef) {
  1350. /* @var $propertyDef \Jackalope\NodeType\PropertyDefinition */
  1351. $this->conn->insert('phpcr_type_props', array(
  1352. 'node_type_id' => $nodeTypeId,
  1353. 'name' => $propertyDef->getName(),
  1354. 'protected' => $propertyDef->isProtected() ? 1 : 0,
  1355. 'mandatory' => $propertyDef->isMandatory() ? 1 : 0,
  1356. 'auto_created' => $propertyDef->isAutoCreated() ? 1 : 0,
  1357. 'on_parent_version' => $propertyDef->getOnParentVersion(),
  1358. 'multiple' => $propertyDef->isMultiple() ? 1 : 0,
  1359. 'fulltext_searchable' => $propertyDef->isFullTextSearchable() ? 1 : 0,
  1360. 'query_orderable' => $propertyDef->isQueryOrderable() ? 1 : 0,
  1361. 'required_type' => $propertyDef->getRequiredType(),
  1362. 'query_operators' => 0, // transform to bitmask
  1363. 'default_value' => $propertyDef->getDefaultValues() ? current($propertyDef->getDefaultValues()) : null,
  1364. ));
  1365. }
  1366. }
  1367. if ($childDefs = $type->getDeclaredChildNodeDefinitions()) {
  1368. foreach ($childDefs as $childDef) {
  1369. /* @var $propertyDef \PHPCR\NodeType\NodeDefinitionInterface */
  1370. $this->conn->insert('phpcr_type_childs', array(
  1371. 'node_type_id' => $nodeTypeId,
  1372. 'name' => $childDef->getName(),
  1373. 'protected' => $childDef->isProtected() ? 1 : 0,
  1374. 'mandatory' => $childDef->isMandatory() ? 1 : 0,
  1375. 'auto_created' => $childDef->isAutoCreated() ? 1 : 0,
  1376. 'on_parent_version' => $childDef->getOnParentVersion(),
  1377. 'primary_types' => implode(' ', $childDef->getRequiredPrimaryTypeNames() ?: array()),
  1378. 'default_type' => $childDef->getDefaultPrimaryTypeName(),
  1379. ));
  1380. }
  1381. }
  1382. }
  1383. }
  1384. /**
  1385. * {@inheritDoc}
  1386. */
  1387. public function setNodeTypeManager($nodeTypeManager)
  1388. {
  1389. $this->nodeTypeManager = $nodeTypeManager;
  1390. }
  1391. /**
  1392. * {@inheritDoc}
  1393. */
  1394. public function cloneFrom($srcWorkspace, $srcAbsPath, $destAbsPath, $removeExisting)
  1395. {
  1396. throw new NotImplementedException('Cloning nodes is not implemented yet');
  1397. }
  1398. /**
  1399. * {@inheritDoc}
  1400. */
  1401. public function getBinaryStream($path)
  1402. {
  1403. $this->assertLoggedIn();
  1404. $nodePath = $this->getParentPath($path);
  1405. $propertyName = ltrim(str_replace($nodePath, '', $path), '/'); // i dont know why trim here :/
  1406. $nodeId = $this->pathExists($nodePath);
  1407. $data = $this->conn->fetchAll(
  1408. 'SELECT data, idx FROM phpcr_binarydata WHERE node_id = ? AND property_name = ? AND workspace_name = ?',
  1409. array($nodeId, $propertyName, $this->workspaceName)
  1410. );
  1411. $streams = array();
  1412. foreach ($data as $row) {
  1413. if (is_resource($row['data'])) {
  1414. $stream = $row['data'];
  1415. } else {
  1416. $stream = fopen('php://memory', 'rwb+');
  1417. fwrite($stream, $row['data']);
  1418. rewind($stream);
  1419. }
  1420. $streams[] = $stream;
  1421. }
  1422. // TODO even a multi value field could have only one value stored
  1423. // we need to also fetch if the property is multi valued instead of this count() check
  1424. if (count($data) > 1) {
  1425. return $streams;
  1426. }
  1427. return reset($streams);
  1428. }
  1429. /**
  1430. * {@inheritDoc}
  1431. */
  1432. public function getProperty($path)
  1433. {
  1434. throw new NotImplementedException('Getting properties by path is implemented yet');
  1435. }
  1436. /**
  1437. * {@inheritDoc}
  1438. */
  1439. public function query(Query $query)
  1440. {
  1441. $this->assertLoggedIn();
  1442. $limit = $query->getLimit();
  1443. $offset = $query->getOffset();
  1444. if (null !== $offset && null == $limit &&
  1445. ($this->conn->getDatabasePlatform() instanceof MySqlPlatform
  1446. || $this->conn->getDatabasePlatform() instanceof SqlitePlatform)
  1447. ) {
  1448. $limit = PHP_INT_MAX;
  1449. }
  1450. if (!$query instanceof QueryObjectModelInterface) {
  1451. $parser = new Sql2ToQomQueryConverter($this->factory->get('Query\QOM\QueryObjectModelFactory'));
  1452. try {
  1453. $query = $parser->parse($query->getStatement());
  1454. } catch (\Exception $e) {
  1455. throw new InvalidQueryException('Invalid query: '.$query->getStatement());
  1456. }
  1457. }
  1458. $source = $query->getSource();
  1459. if (!($source instanceof SelectorInterface)) {
  1460. throw new NotImplementedException("Only Selector Sources are supported for now, but no Join.");
  1461. }
  1462. // TODO: this check is only relevant for Selector, not for Join. should we push it into the walker?
  1463. $nodeType = $source->getNodeTypeName();
  1464. if (!$this->nodeTypeManager->hasNodeType($nodeType)) {
  1465. $msg = 'Selected node type does not exist: ' . $nodeType;
  1466. if ($alias = $source->getSelectorName()) {
  1467. $msg .= ' AS ' . $alias;
  1468. }
  1469. throw new InvalidQueryException($msg);
  1470. }
  1471. $qomWalker = new QOMWalker($this->nodeTypeManager, $this->conn, $this->getNamespaces());
  1472. $sql = $qomWalker->walkQOMQuery($query);
  1473. $sql = $this->conn->getDatabasePlatform()->modifyLimitQuery($sql, $limit, $offset);
  1474. $data = $this->conn->fetchAll($sql, array($this->workspaceName));
  1475. // The list of columns is required to filter each records props
  1476. $columns = array();
  1477. /** @var $column \PHPCR\Query\QOM\ColumnInterface */
  1478. foreach ($query->getColumns() as $column) {
  1479. $columns[$column->getPropertyName()] = $column->getSelectorName();
  1480. }
  1481. // TODO: this needs update once we implement join
  1482. $selector = $source->getSelectorName();
  1483. if (null === $selector) {
  1484. $selector = $source->getNodeTypeName();
  1485. }
  1486. if (empty($columns)) {
  1487. $columns = array(
  1488. 'jcr:createdBy' => $selector,
  1489. 'jcr:created' => $selector,
  1490. );
  1491. }
  1492. $columns['jcr:primaryType'] = $selector;
  1493. $results = array();
  1494. // This block feels really clunky - maybe this should be a QueryResultFormatter class?
  1495. foreach ($data as $row) {
  1496. $result = array(
  1497. array('dcr:name' => 'jcr:path', 'dcr:value' => $row['path'], 'dcr:selectorName' => $row['type']),
  1498. array('dcr:name' => 'jcr:score', 'dcr:value' => 0, 'dcr:selectorName' => $row['type'])
  1499. );
  1500. // extract only the properties that have been requested in the query
  1501. $props = static::xmlToProps($row['props'], function ($name) use ($columns) {
  1502. return array_key_exists($name, $columns);
  1503. });
  1504. foreach ($columns AS $columnName => $columnPrefix) {
  1505. $result[] = array(
  1506. 'dcr:name' => null === $columnPrefix ? $columnName : "{$columnPrefix}.{$columnName}",
  1507. 'dcr:value' => array_key_exists($columnName, $props) ? $props[$columnName] : null,
  1508. 'dcr:selectorName' => $columnPrefix ?: $selector,
  1509. );
  1510. }
  1511. $results[] = $result;
  1512. }
  1513. return $results;
  1514. }
  1515. /**
  1516. * {@inheritDoc}
  1517. */
  1518. public function registerNamespace($prefix, $uri)
  1519. {
  1520. if (isset($this->namespaces[$prefix]) && $this->namespaces[$prefix] === $uri) {
  1521. return;
  1522. }
  1523. $this->conn->delete('phpcr_namespaces', array('prefix' => $prefix));
  1524. $this->conn->delete('phpcr_namespaces', array('uri' => $uri));
  1525. $this->conn->insert('phpcr_namespaces', array(
  1526. 'prefix' => $prefix,
  1527. 'uri' => $uri,
  1528. ));
  1529. if (!empty($this->namespaces)) {
  1530. $this->namespaces[$prefix] = $uri;
  1531. }
  1532. }
  1533. /**
  1534. * {@inheritDoc}
  1535. */
  1536. public function unregisterNamespace($prefix)
  1537. {
  1538. $this->conn->delete('phpcr_namespaces', array('prefix' => $prefix));
  1539. if (!empty($this->namespaces)) {
  1540. unset($this->namespaces[$prefix]);
  1541. }
  1542. }
  1543. /**
  1544. * {@inheritDoc}
  1545. */
  1546. public function getReferences($path, $name = null)
  1547. {
  1548. return $this->getNodeReferences($path, $name, false);
  1549. }
  1550. /**
  1551. * {@inheritDoc}
  1552. */
  1553. public function getWeakReferences($path, $name = null)
  1554. {
  1555. return $this->getNodeReferences($path, $name, true);
  1556. }
  1557. /**
  1558. * @param string $path the path for which we need the references
  1559. * @param string $name the name of the referencing properties or null for all
  1560. * @param bool $weak_reference whether to get weak or strong references
  1561. *
  1562. * @return array list of paths to nodes that reference $path
  1563. */
  1564. private function getNodeReferences($path, $name = null, $weakReference = false)
  1565. {
  1566. $targetId = $this->pathExists($path);
  1567. $type = $weakReference ? PropertyType::WEAKREFERENCE : PropertyType::REFERENCE;
  1568. $query = "SELECT CONCAT(n.path, '/', fk.source_property_name) as path, fk.source_property_name FROM phpcr_nodes n".
  1569. ' INNER JOIN phpcr_nodes_foreignkeys fk ON n.id = fk.source_id'.
  1570. ' WHERE fk.target_id = ? AND fk.type = ?';
  1571. $properties = $this->conn->fetchAll($query, array($targetId, $type));
  1572. $references = array();
  1573. foreach ($properties as $property) {
  1574. if (null === $name || $property['source_property_name'] == $name) {
  1575. $references[] = $property['path'];
  1576. }
  1577. }
  1578. return $references;
  1579. }
  1580. /**
  1581. * {@inheritDoc}
  1582. */
  1583. public function beginTransaction()
  1584. {
  1585. if ($this->inTransaction) {
  1586. throw new RepositoryException('Begin transaction failed: transaction already open');
  1587. }
  1588. $this->assertLoggedIn();
  1589. try {
  1590. $this->conn->beginTransaction();
  1591. $this->inTransaction = true;
  1592. } catch (\Exception $e) {
  1593. throw new RepositoryException('Begin transaction failed: '.$e->getMessage());
  1594. }
  1595. }
  1596. /**
  1597. * {@inheritDoc}
  1598. */
  1599. public function commitTransaction()
  1600. {
  1601. if (!$this->inTransaction) {
  1602. throw new RepositoryException('Commit transaction failed: no transaction open');
  1603. }
  1604. $this->assertLoggedIn();
  1605. try {
  1606. $this->inTransaction = false;
  1607. $this->conn->commit();
  1608. } catch (\Exception $e) {
  1609. throw new RepositoryException('Commit transaction failed: ' . $e->getMessage());
  1610. }
  1611. }
  1612. /**
  1613. * {@inheritDoc}
  1614. */
  1615. public function rollbackTransaction()
  1616. {
  1617. if (!$this->inTransaction) {
  1618. throw new RepositoryException('Rollback transaction failed: no transaction open');
  1619. }
  1620. $this->assertLoggedIn();
  1621. try {
  1622. $this->inTransaction = false;
  1623. $this->namespaces = array();
  1624. $this->conn->rollback();
  1625. } catch (\Exception $e) {
  1626. throw new RepositoryException('Rollback transaction failed: ' . $e->getMessage());
  1627. }
  1628. }
  1629. /**
  1630. * Sets the default transaction timeout
  1631. *
  1632. * @param int $seconds The value of the timeout in seconds
  1633. */
  1634. public function setTransactionTimeout($seconds)
  1635. {
  1636. $this->assertLoggedIn();
  1637. throw new NotImplementedException("Setting a transaction timeout is not yet implemented");
  1638. }
  1639. /**
  1640. * {@inheritDoc}
  1641. */
  1642. public function prepareSave()
  1643. {
  1644. $this->conn->beginTransaction();
  1645. }
  1646. /**
  1647. * {@inheritDoc}
  1648. */
  1649. public function finishSave()
  1650. {
  1651. $this->conn->commit();
  1652. }
  1653. /**
  1654. * {@inheritDoc}
  1655. */
  1656. public function rollbackSave()
  1657. {
  1658. $this->conn->rollback();
  1659. }
  1660. }