vendor/jackalope/jackalope-doctrine-dbal/src/Jackalope/Transport/DoctrineDBAL/Client.php line 2453

Open in your IDE?
  1. <?php
  2. namespace Jackalope\Transport\DoctrineDBAL;
  3. use ArrayObject;
  4. use Closure;
  5. use DateTime;
  6. use DateTimeZone;
  7. use Doctrine\DBAL\Connection;
  8. use Doctrine\DBAL\Driver\PDO\Connection as PDOConnection;
  9. use Doctrine\DBAL\Exception as DBALException;
  10. use Doctrine\DBAL\ParameterType;
  11. use Doctrine\DBAL\Platforms\MySQLPlatform;
  12. use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
  13. use Doctrine\DBAL\Platforms\PostgreSQL94Platform;
  14. use Doctrine\DBAL\Platforms\SqlitePlatform;
  15. use DOMDocument;
  16. use DOMElement;
  17. use DOMXPath;
  18. use Exception;
  19. use InvalidArgumentException;
  20. use Jackalope\FactoryInterface;
  21. use Jackalope\Node;
  22. use Jackalope\NodeType\NodeProcessor;
  23. use Jackalope\NodeType\NodeTypeDefinition;
  24. use Jackalope\NodeType\NodeTypeManager;
  25. use Jackalope\NotImplementedException;
  26. use Jackalope\Property;
  27. use Jackalope\Query\QOM\QueryObjectModelFactory;
  28. use Jackalope\Query\Query;
  29. use Jackalope\Transport\BaseTransport;
  30. use Jackalope\Transport\DoctrineDBAL\Query\QOMWalker;
  31. use Jackalope\Transport\DoctrineDBAL\XmlParser\XmlToPropsParser;
  32. use Jackalope\Transport\MoveNodeOperation;
  33. use Jackalope\Transport\NodeTypeManagementInterface;
  34. use Jackalope\Transport\QueryInterface as QueryTransport;
  35. use Jackalope\Transport\StandardNodeTypes;
  36. use Jackalope\Transport\TransactionInterface;
  37. use Jackalope\Transport\WorkspaceManagementInterface;
  38. use Jackalope\Transport\WritingInterface;
  39. use PDO;
  40. use PHPCR\AccessDeniedException;
  41. use PHPCR\CredentialsInterface;
  42. use PHPCR\ItemExistsException;
  43. use PHPCR\ItemNotFoundException;
  44. use PHPCR\LoginException;
  45. use PHPCR\NamespaceException;
  46. use PHPCR\NamespaceRegistryInterface as NS;
  47. use PHPCR\NodeType\ConstraintViolationException;
  48. use PHPCR\NodeType\NodeDefinitionInterface;
  49. use PHPCR\NodeType\NodeTypeExistsException;
  50. use PHPCR\NodeType\NoSuchNodeTypeException;
  51. use PHPCR\NodeType\PropertyDefinitionInterface;
  52. use PHPCR\NoSuchWorkspaceException;
  53. use PHPCR\PathNotFoundException;
  54. use PHPCR\PropertyType;
  55. use PHPCR\Query\InvalidQueryException;
  56. use PHPCR\Query\QOM\QueryObjectModelConstantsInterface as QOM;
  57. use PHPCR\Query\QOM\QueryObjectModelInterface;
  58. use PHPCR\Query\QOM\SelectorInterface;
  59. use PHPCR\Query\QueryInterface;
  60. use PHPCR\ReferentialIntegrityException;
  61. use PHPCR\RepositoryException;
  62. use PHPCR\RepositoryInterface;
  63. use PHPCR\SimpleCredentials;
  64. use PHPCR\UnsupportedRepositoryOperationException;
  65. use PHPCR\Util\PathHelper;
  66. use PHPCR\Util\QOM\Sql2ToQomQueryConverter;
  67. use PHPCR\Util\UUIDHelper;
  68. use PHPCR\Util\ValueConverter;
  69. use stdClass;
  70. /**
  71.  * Class to handle the communication between Jackalope and RDBMS via Doctrine DBAL.
  72.  *
  73.  * @license http://www.apache.org/licenses Apache License Version 2.0, January 2004
  74.  * @license http://opensource.org/licenses/MIT MIT License
  75.  *
  76.  * @author Benjamin Eberlei <kontakt@beberlei.de>
  77.  * @author Lukas Kahwe Smith <smith@pooteeweet.org>
  78.  */
  79. class Client extends BaseTransport implements QueryTransportWritingInterfaceWorkspaceManagementInterfaceNodeTypeManagementInterfaceTransactionInterface
  80. {
  81.     /**
  82.      * SQlite can only handle a maximum of 999 parameters inside an IN statement
  83.      * see https://github.com/jackalope/jackalope-doctrine-dbal/pull/149/files#diff-a3a0165ed79ca1ba3513ec5ecd59ec56R707
  84.      */
  85.     const SQLITE_MAXIMUM_IN_PARAM_COUNT 999;
  86.     /**
  87.      * The factory to instantiate objects
  88.      *
  89.      * @var FactoryInterface
  90.      */
  91.     protected $factory;
  92.     /**
  93.      * @var ValueConverter
  94.      */
  95.     protected $valueConverter;
  96.     /**
  97.      * @var Connection
  98.      */
  99.     private $conn;
  100.     /**
  101.      * @var Closure
  102.      */
  103.     private $uuidGenerator;
  104.     /**
  105.      * @var bool
  106.      */
  107.     private $loggedIn false;
  108.     /**
  109.      * @var SimpleCredentials
  110.      */
  111.     private $credentials;
  112.     /**
  113.      * @var string
  114.      */
  115.     protected $workspaceName;
  116.     /**
  117.      * @var array
  118.      */
  119.     private $nodeIdentifiers = [];
  120.     /**
  121.      * @var NodeTypeManager
  122.      */
  123.     private $nodeTypeManager;
  124.     /**
  125.      * @var bool
  126.      */
  127.     protected $inTransaction false;
  128.     /**
  129.      * Check if an initial request on login should be send to check if repository exists
  130.      * This is according to the JCR specifications and set to true by default
  131.      *
  132.      * @see setCheckLoginOnServer
  133.      *
  134.      * @var bool
  135.      */
  136.     private $checkLoginOnServer true;
  137.     /**
  138.      * Using an ArrayObject here so that we can pass this into the NodeProcessor by reference more elegantly
  139.      *
  140.      * @var null|ArrayObject
  141.      */
  142.     protected $namespaces;
  143.     /**
  144.      * @var null|array The namespaces at initial state when making changes to the namespaces, in case of rollback.
  145.      */
  146.     private $originalNamespaces;
  147.     /**
  148.      * The core namespaces defined in JCR
  149.      *
  150.      * @var array
  151.      */
  152.     private $coreNamespaces = [
  153.         NS::PREFIX_EMPTY => NS::NAMESPACE_EMPTY,
  154.         NS::PREFIX_JCR => NS::NAMESPACE_JCR,
  155.         NS::PREFIX_NT => NS::NAMESPACE_NT,
  156.         NS::PREFIX_MIX => NS::NAMESPACE_MIX,
  157.         NS::PREFIX_XML => NS::NAMESPACE_XML,
  158.         NS::PREFIX_SV => NS::NAMESPACE_SV,
  159.     ];
  160.     /**
  161.      * @var string|null
  162.      */
  163.     private $sequenceNodeName;
  164.     /**
  165.      * @var string|null
  166.      */
  167.     private $sequenceTypeName;
  168.     /**
  169.      * @var array
  170.      */
  171.     private $referencesToUpdate = [];
  172.     /**
  173.      * @var array
  174.      */
  175.     private $referenceTables = [
  176.         PropertyType::REFERENCE => 'phpcr_nodes_references',
  177.         PropertyType::WEAKREFERENCE => 'phpcr_nodes_weakreferences',
  178.     ];
  179.     /**
  180.      * @var array
  181.      */
  182.     private $referencesToDelete = [];
  183.     /**
  184.      * @var boolean
  185.      */
  186.     private $connectionInitialized false;
  187.     /**
  188.      * @var NodeProcessor
  189.      */
  190.     private $nodeProcessor;
  191.     /**
  192.      * @var string
  193.      */
  194.     private $caseSensitiveEncoding null;
  195.     /**
  196.      * @param FactoryInterface $factory
  197.      * @param Connection $conn
  198.      */
  199.     public function __construct(FactoryInterface $factoryConnection $conn)
  200.     {
  201.         $this->factory $factory;
  202.         $this->valueConverter $this->factory->get(ValueConverter::class);
  203.         $this->conn $conn;
  204.     }
  205.     /**
  206.      * @TODO: move to "SqlitePlatform" and rename to "registerExtraFunctions"?
  207.      */
  208.     private function registerSqliteFunctions(PDO $sqliteConnection): void
  209.     {
  210.         $sqliteConnection->sqliteCreateFunction(
  211.             'EXTRACTVALUE',
  212.             function ($string$expression) {
  213.                 if (null === $string) {
  214.                     return null;
  215.                 }
  216.                 $dom = new DOMDocument('1.0''UTF-8');
  217.                 $dom->loadXML($string);
  218.                 $xpath = new DOMXPath($dom);
  219.                 $list $xpath->evaluate($expression);
  220.                 if (!is_object($list)) {
  221.                     return $list;
  222.                 }
  223.                 // @TODO: don't know if there are expressions returning more then one row
  224.                 if ($list->length 0) {
  225.                     // @TODO: why it can happen that we do not have a type? https://github.com/phpcr/phpcr-api-tests/pull/132
  226.                     $type is_object($list->item(0)->parentNode->attributes->getNamedItem('type')) ? $list->item(
  227.                         0
  228.                     )->parentNode->attributes->getNamedItem('type')->value null;
  229.                     $content $list->item(0)->textContent;
  230.                     switch ($type) {
  231.                         case 'long':
  232.                             return (int)$content;
  233.                         case 'double':
  234.                             return (double)$content;
  235.                         default:
  236.                             return $content;
  237.                     }
  238.                 }
  239.                 // @TODO: don't know if return value is right
  240.                 return null;
  241.             },
  242.             2
  243.         );
  244.         $sqliteConnection->sqliteCreateFunction(
  245.             'CONCAT',
  246.             function () {
  247.                 return implode(''func_get_args());
  248.             }
  249.         );
  250.     }
  251.     /**
  252.      * @return Connection
  253.      */
  254.     public function getConnection()
  255.     {
  256.         $this->initConnection();
  257.         return $this->conn;
  258.     }
  259.     /**
  260.      * Set the UUID generator to use. If not set, the phpcr-utils UUIDHelper
  261.      * will be used.
  262.      *
  263.      * @param Closure $generator
  264.      */
  265.     public function setUuidGenerator(Closure $generator)
  266.     {
  267.         $this->uuidGenerator $generator;
  268.     }
  269.     /**
  270.      * @return callable a uuid generator function.
  271.      */
  272.     protected function getUuidGenerator()
  273.     {
  274.         if (!$this->uuidGenerator) {
  275.             $this->uuidGenerator = function () {
  276.                 return UUIDHelper::generateUUID();
  277.             };
  278.         }
  279.         return $this->uuidGenerator;
  280.     }
  281.     /**
  282.      * @return string a universally unique id.
  283.      */
  284.     protected function generateUuid()
  285.     {
  286.         // php 5.3 compatibility, no direct execution of this function.
  287.         $g $this->getUuidGenerator();
  288.         return $g();
  289.     }
  290.     /**
  291.      * {@inheritDoc}
  292.      *
  293.      * @throws NotImplementedException
  294.      */
  295.     public function createWorkspace($name$srcWorkspace null)
  296.     {
  297.         if (null !== $srcWorkspace) {
  298.             throw new NotImplementedException('Creating workspace as clone of existing workspace not supported');
  299.         }
  300.         if ($this->workspaceExists($name)) {
  301.             throw new RepositoryException("Workspace '$name' already exists");
  302.         }
  303.         try {
  304.             $this->getConnection()->insert('phpcr_workspaces', ['name' => $name]);
  305.         } catch (Exception $e) {
  306.             throw new RepositoryException("Couldn't create Workspace '$name': " $e->getMessage(), 0$e);
  307.         }
  308.         $this->getConnection()->insert(
  309.             'phpcr_nodes',
  310.             [
  311.                 'path' => '/',
  312.                 'parent' => '',
  313.                 'workspace_name' => $name,
  314.                 'identifier' => $this->generateUuid(),
  315.                 'type' => 'nt:unstructured',
  316.                 'local_name' => '',
  317.                 'namespace' => '',
  318.                 'props' => '<?xml version="1.0" encoding="UTF-8"?>
  319. <sv:node xmlns:' NS::PREFIX_MIX '="' NS::NAMESPACE_MIX '" xmlns:' NS::PREFIX_NT '="' NS::NAMESPACE_NT '" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:' NS::PREFIX_JCR '="' NS::NAMESPACE_JCR '" xmlns:' NS::PREFIX_SV '="' NS::NAMESPACE_SV '" xmlns:rep="internal" />',
  320.                 // TODO compute proper value
  321.                 'depth' => 0,
  322.             ]
  323.         );
  324.     }
  325.     /**
  326.      * {@inheritDoc}
  327.      */
  328.     public function deleteWorkspace($name)
  329.     {
  330.         if (!$this->workspaceExists($name)) {
  331.             throw new RepositoryException("Workspace '$name' cannot be deleted as it does not exist");
  332.         }
  333.         try {
  334.             $this->getConnection()->delete('phpcr_workspaces', ['name' => $name]);
  335.         } catch (Exception $e) {
  336.             throw new RepositoryException("Couldn't delete workspace '$name': " $e->getMessage(), 0$e);
  337.         }
  338.         try {
  339.             $this->getConnection()->delete('phpcr_nodes', ['workspace_name' => $name]);
  340.         } catch (Exception $e) {
  341.             throw new RepositoryException("Couldn't delete nodes in workspace '$name': " $e->getMessage(), 0$e);
  342.         }
  343.         try {
  344.             $this->getConnection()->delete('phpcr_binarydata', ['workspace_name' => $name]);
  345.         } catch (Exception $e) {
  346.             throw new RepositoryException(
  347.                 "Couldn't delete binary data in workspace '$name': " $e->getMessage(),
  348.                 0,
  349.                 $e
  350.             );
  351.         }
  352.     }
  353.     /**
  354.      * {@inheritDoc}
  355.      *
  356.      * @throws NotImplementedException
  357.      * @throws AccessDeniedException
  358.      * @throws UnsupportedRepositoryOperationException
  359.      */
  360.     public function login(CredentialsInterface $credentials null$workspaceName null)
  361.     {
  362.         $this->credentials $credentials;
  363.         $this->workspaceName $workspaceName ?: 'default';
  364.         if (!$this->checkLoginOnServer) {
  365.             return $this->workspaceName;
  366.         }
  367.         if (!$this->workspaceExists($this->workspaceName)) {
  368.             if ('default' !== $this->workspaceName) {
  369.                 throw new NoSuchWorkspaceException("Requested workspace: '{$this->workspaceName}'");
  370.             }
  371.             // Create default workspace if it not exists
  372.             $this->createWorkspace($this->workspaceName);
  373.         }
  374.         $this->loggedIn true;
  375.         return $this->workspaceName;
  376.     }
  377.     /**
  378.      * {@inheritDoc}
  379.      */
  380.     public function logout()
  381.     {
  382.         if ($this->loggedIn) {
  383.             $this->loggedIn false;
  384.             $this->conn->close();
  385.             $this->conn null;
  386.         }
  387.     }
  388.     /**
  389.      * Configure whether to check if we are logged in before doing a request.
  390.      *
  391.      * Will improve error reporting at the cost of some round trips.
  392.      */
  393.     public function setCheckLoginOnServer($bool)
  394.     {
  395.         $this->checkLoginOnServer $bool;
  396.     }
  397.     /**
  398.      * This will control the collate which is being used on MySQL when querying nodes. It will be autodetected by just
  399.      * appending _bin to the current charset, which is good enough in most cases.
  400.      *
  401.      * @param string $encoding
  402.      */
  403.     public function setCaseSensitiveEncoding($encoding)
  404.     {
  405.         $this->caseSensitiveEncoding $encoding;
  406.     }
  407.     /**
  408.      * Returns the collate which is being used on MySQL when querying nodes.
  409.      *
  410.      * @return string
  411.      */
  412.     private function getCaseSensitiveEncoding()
  413.     {
  414.         if (null !== $this->caseSensitiveEncoding) {
  415.             return $this->caseSensitiveEncoding;
  416.         }
  417.         $params $this->conn->getParams();
  418.         $charset = isset($params['charset']) ? $params['charset'] : 'utf8';
  419.         if (isset($params['defaultTableOptions']) && isset($params['defaultTableOptions']['collate'])) {
  420.             return $this->caseSensitiveEncoding $params['defaultTableOptions']['collate'];
  421.         }
  422.         return $this->caseSensitiveEncoding $charset === 'binary' $charset $charset '_bin';
  423.     }
  424.     protected function workspaceExists($workspaceName)
  425.     {
  426.         try {
  427.             $query 'SELECT 1 FROM phpcr_workspaces WHERE name = ?';
  428.             $result $this->getConnection()->fetchFirstColumn($query, [$workspaceName]);
  429.         } catch (Exception $e) {
  430.             if ($e instanceof DBALException) {
  431.                 if (1045 === $e->getCode()) {
  432.                     throw new LoginException('Access denied with your credentials: ' $e->getMessage());
  433.                 }
  434.                 if ('42S02' === $e->getCode()) {
  435.                     throw new RepositoryException(
  436.                         'You did not properly set up the database for the repository. See README.md for more information. Message from backend: ' $e->getMessage(
  437.                         )
  438.                     );
  439.                 }
  440.                 throw new RepositoryException('Unexpected error talking to the backend: ' $e->getMessage(), 0$e);
  441.             }
  442.             throw $e;
  443.         }
  444.         return $result;
  445.     }
  446.     /**
  447.      * Ensure that we are currently logged in, executing the login in case we
  448.      * did lazy login.
  449.      *
  450.      * @throws NotImplementedException
  451.      * @throws AccessDeniedException
  452.      * @throws LoginException
  453.      * @throws NoSuchWorkspaceException
  454.      * @throws UnsupportedRepositoryOperationException
  455.      * @throws RepositoryException if this transport is not logged in.
  456.      */
  457.     protected function assertLoggedIn()
  458.     {
  459.         if (!$this->loggedIn) {
  460.             if (!$this->checkLoginOnServer && $this->workspaceName) {
  461.                 $this->checkLoginOnServer true;
  462.                 if ($this->login($this->credentials$this->workspaceName)) {
  463.                     return;
  464.                 }
  465.             }
  466.             throw new RepositoryException('You need to be logged in for this operation');
  467.         }
  468.     }
  469.     /**
  470.      * {@inheritDoc}
  471.      */
  472.     public function getRepositoryDescriptors()
  473.     {
  474.         return [
  475.             RepositoryInterface::IDENTIFIER_STABILITY => RepositoryInterface::IDENTIFIER_STABILITY_INDEFINITE_DURATION,
  476.             RepositoryInterface::REP_NAME_DESC => 'jackalope_doctrine_dbal',
  477.             RepositoryInterface::REP_VENDOR_DESC => 'Jackalope Community',
  478.             RepositoryInterface::REP_VENDOR_URL_DESC => 'http://github.com/jackalope',
  479.             RepositoryInterface::REP_VERSION_DESC => '1.1.0-DEV',
  480.             RepositoryInterface::SPEC_NAME_DESC => 'Content Repository for PHP',
  481.             RepositoryInterface::SPEC_VERSION_DESC => '2.1',
  482.             RepositoryInterface::NODE_TYPE_MANAGEMENT_AUTOCREATED_DEFINITIONS_SUPPORTED => true,
  483.             RepositoryInterface::NODE_TYPE_MANAGEMENT_INHERITANCE => RepositoryInterface::NODE_TYPE_MANAGEMENT_INHERITANCE_SINGLE,
  484.             RepositoryInterface::NODE_TYPE_MANAGEMENT_MULTIPLE_BINARY_PROPERTIES_SUPPORTED => true,
  485.             RepositoryInterface::NODE_TYPE_MANAGEMENT_MULTIVALUED_PROPERTIES_SUPPORTED => true,
  486.             RepositoryInterface::NODE_TYPE_MANAGEMENT_ORDERABLE_CHILD_NODES_SUPPORTED => true,
  487.             RepositoryInterface::NODE_TYPE_MANAGEMENT_OVERRIDES_SUPPORTED => false,
  488.             RepositoryInterface::NODE_TYPE_MANAGEMENT_PRIMARY_ITEM_NAME_SUPPORTED => true,
  489.             RepositoryInterface::NODE_TYPE_MANAGEMENT_PROPERTY_TYPES => true,
  490.             RepositoryInterface::NODE_TYPE_MANAGEMENT_RESIDUAL_DEFINITIONS_SUPPORTED => false,
  491.             RepositoryInterface::NODE_TYPE_MANAGEMENT_SAME_NAME_SIBLINGS_SUPPORTED => false,
  492.             RepositoryInterface::NODE_TYPE_MANAGEMENT_UPDATE_IN_USE_SUPPORTED => false,
  493.             RepositoryInterface::NODE_TYPE_MANAGEMENT_VALUE_CONSTRAINTS_SUPPORTED => false,
  494.             RepositoryInterface::OPTION_ACCESS_CONTROL_SUPPORTED => false,
  495.             RepositoryInterface::OPTION_ACTIVITIES_SUPPORTED => false,
  496.             RepositoryInterface::OPTION_BASELINES_SUPPORTED => false,
  497.             RepositoryInterface::OPTION_JOURNALED_OBSERVATION_SUPPORTED => false,
  498.             RepositoryInterface::OPTION_LIFECYCLE_SUPPORTED => false,
  499.             RepositoryInterface::OPTION_LOCKING_SUPPORTED => false,
  500.             RepositoryInterface::OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED => true,
  501.             RepositoryInterface::OPTION_NODE_TYPE_MANAGEMENT_SUPPORTED => true,
  502.             RepositoryInterface::OPTION_OBSERVATION_SUPPORTED => false,
  503.             RepositoryInterface::OPTION_RETENTION_SUPPORTED => false,
  504.             RepositoryInterface::OPTION_SHAREABLE_NODES_SUPPORTED => false,
  505.             RepositoryInterface::OPTION_SIMPLE_VERSIONING_SUPPORTED => false,
  506.             RepositoryInterface::OPTION_TRANSACTIONS_SUPPORTED => true,
  507.             RepositoryInterface::OPTION_UNFILED_CONTENT_SUPPORTED => true,
  508.             RepositoryInterface::OPTION_UPDATE_MIXIN_NODETYPES_SUPPORTED => true,
  509.             RepositoryInterface::OPTION_UPDATE_PRIMARY_NODETYPE_SUPPORTED => true,
  510.             RepositoryInterface::OPTION_VERSIONING_SUPPORTED => false,
  511.             RepositoryInterface::OPTION_WORKSPACE_MANAGEMENT_SUPPORTED => true,
  512.             RepositoryInterface::OPTION_XML_EXPORT_SUPPORTED => true,
  513.             RepositoryInterface::OPTION_XML_IMPORT_SUPPORTED => true,
  514.             RepositoryInterface::QUERY_FULL_TEXT_SEARCH_SUPPORTED => true,
  515.             RepositoryInterface::QUERY_CANCEL_SUPPORTED => false,
  516.             RepositoryInterface::QUERY_JOINS => RepositoryInterface::QUERY_JOINS_NONE,
  517.             RepositoryInterface::QUERY_LANGUAGES => [QueryInterface::JCR_SQL2QueryInterface::JCR_JQOM],
  518.             RepositoryInterface::QUERY_STORED_QUERIES_SUPPORTED => false,
  519.             RepositoryInterface::WRITE_SUPPORTED => true,
  520.         ];
  521.     }
  522.     /**
  523.      * Get the registered namespace prefixes
  524.      *
  525.      * @return array
  526.      */
  527.     private function getNamespacePrefixes()
  528.     {
  529.         return array_keys($this->getNamespaces());
  530.     }
  531.     /**
  532.      * {@inheritDoc}
  533.      */
  534.     public function getNamespaces()
  535.     {
  536.         return (array)$this->getNamespacesObject();
  537.     }
  538.     /**
  539.      * Return the namespaces of the current session as a referenceable ArrayObject.
  540.      *
  541.      * @return ArrayObject
  542.      */
  543.     protected function getNamespacesObject()
  544.     {
  545.         if (null === $this->namespaces) {
  546.             $query 'SELECT prefix, uri FROM phpcr_namespaces';
  547.             $result $this->getConnection()->executeQuery($query);
  548.             $columns $result->fetchAllNumeric();
  549.             $namespaces array_column($columns10);
  550.             $namespaces += $this->coreNamespaces;
  551.             $this->setNamespaces($namespaces);
  552.         }
  553.         return $this->namespaces;
  554.     }
  555.     /**
  556.      * Set the namespaces property to an \ArrayObject instance
  557.      *
  558.      * @param array|ArrayObject $namespaces
  559.      */
  560.     protected function setNamespaces($namespaces)
  561.     {
  562.         if ($this->namespaces instanceof ArrayObject) {
  563.             $this->namespaces->exchangeArray($namespaces);
  564.         } else {
  565.             $this->namespaces = new ArrayObject($namespaces);
  566.         }
  567.     }
  568.     /**
  569.      * Executes an UPDATE on DBAL while ensuring that we never try to send more than 999 parameters to SQLite
  570.      *
  571.      * @param $query
  572.      * @param array $params
  573.      *
  574.      * @throws DBALException
  575.      */
  576.     private function executeChunkedUpdate($query, array $params)
  577.     {
  578.         $types = [Connection::PARAM_INT_ARRAY];
  579.         if ($this->getConnection()->getDatabasePlatform() instanceof SqlitePlatform) {
  580.             foreach (array_chunk($paramsself::SQLITE_MAXIMUM_IN_PARAM_COUNT) as $chunk) {
  581.                 $this->getConnection()->executeUpdate($query, [$chunk], $types);
  582.             }
  583.         } else {
  584.             $this->getConnection()->executeUpdate($query, [$params], $types);
  585.         }
  586.     }
  587.     /**
  588.      * {@inheritDoc}
  589.      *
  590.      * @throws NoSuchWorkspaceException
  591.      * @throws RepositoryException
  592.      * @throws PathNotFoundException
  593.      * @throws ItemExistsException
  594.      * @throws DBALException
  595.      * @throws InvalidArgumentException
  596.      */
  597.     public function copyNode($srcAbsPath$dstAbsPath$srcWorkspace null)
  598.     {
  599.         $this->assertLoggedIn();
  600.         if (null !== $srcWorkspace && !$this->workspaceExists($srcWorkspace)) {
  601.             throw new NoSuchWorkspaceException("Source workspace '$srcWorkspace' does not exist.");
  602.         }
  603.         $srcWorkspace $srcWorkspace ?: $this->workspaceName;
  604.         PathHelper::assertValidAbsolutePath($dstAbsPathtruetrue$this->getNamespacePrefixes());
  605.         $srcNodeId $this->getSystemIdForNode($srcAbsPath$srcWorkspace);
  606.         if (!$srcNodeId) {
  607.             throw new PathNotFoundException("Source path '$srcAbsPath' not found");
  608.         }
  609.         if ($this->getSystemIdForNode($dstAbsPath)) {
  610.             throw new ItemExistsException("Cannot copy to destination path '$dstAbsPath' that already exists.");
  611.         }
  612.         if (!$this->getSystemIdForNode(PathHelper::getParentPath($dstAbsPath))) {
  613.             throw new PathNotFoundException("Parent of the destination path '" $dstAbsPath "' has to exist.");
  614.         }
  615.         // Algorithm:
  616.         // 1. Select all nodes with path $srcAbsPath."%" and iterate them
  617.         // 2. create a new node with path $dstAbsPath + leftovers, with a new uuid. Save old => new uuid
  618.         // 3. copy all properties from old node to new node
  619.         // 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.
  620.         // 5. "May drop mixin types"
  621.         $query 'SELECT * FROM phpcr_nodes WHERE (path = ? OR path LIKE ?) AND workspace_name = ? ORDER BY depth, sort_order';
  622.         $stmt $this->getConnection()->executeQuery($query, [$srcAbsPath$srcAbsPath '/%'$srcWorkspace]);
  623.         $rows $stmt->fetchAllAssociative();
  624.         $uuidMap = [];
  625.         $resultSetUuids = [];
  626.         // first iterate and build up an array of all the UUIDs in the result set
  627.         foreach ($rows as $row) {
  628.             $resultSetUuids[$row['identifier']] = $row['path'];
  629.         }
  630.         // array of references to remap within the copied tree
  631.         $referenceElsToRemap = [];
  632.         // array references that will need updating in the database
  633.         $referencesToUpdate = [];
  634.         foreach ($rows as $row) {
  635.             $newPath str_replace($srcAbsPath$dstAbsPath$row['path']);
  636.             $stringDom = new DOMDocument('1.0''UTF-8');
  637.             $stringDom->loadXML($row['props']);
  638.             $numericalDom null;
  639.             if ($row['numerical_props']) {
  640.                 $numericalDom = new DOMDocument('1.0''UTF-8');
  641.                 $numericalDom->loadXML($row['numerical_props']);
  642.             }
  643.             $propsData = [
  644.                 'stringDom' => $stringDom,
  645.                 'numericalDom' => $numericalDom,
  646.                 'references' => [],
  647.             ];
  648.             $xpath = new DOMXpath($stringDom);
  649.             $referenceEls $xpath->query(
  650.                 './/sv:property[@sv:type="reference" or @sv:type="Reference" or @sv:type="weakreference" or @sv:type="WeakReference"]'
  651.             );
  652.             $references = [];
  653.             foreach ($referenceEls as $referenceEl) {
  654.                 $propName $referenceEl->getAttribute('sv:name');
  655.                 $values = [];
  656.                 foreach ($xpath->query('./sv:value'$referenceEl) as $valueEl) {
  657.                     $values[] = $valueEl->nodeValue;
  658.                 }
  659.                 $references[$propName] = [
  660.                     'type' => PropertyType::valueFromName($referenceEl->getAttribute('sv:type')),
  661.                     'values' => $values,
  662.                 ];
  663.                 if (isset($resultSetUuids[$referenceEl->nodeValue])) {
  664.                     $referenceElsToRemap[] = [$referenceEl$newPath$row['type'], $propsData];
  665.                 }
  666.             }
  667.             $originalUuid $row['identifier'];
  668.             // when copying a node, the copy is always a new node. set $isNewNode to true
  669.             $newNodeId $this->syncNode(null$newPath$row['type'], true, [], $propsData);
  670.             if ($references) {
  671.                 $referencesToUpdate[$newNodeId] = [
  672.                     'path' => $row['path'],
  673.                     'properties' => $references,
  674.                 ];
  675.             }
  676.             $newUuid $this->nodeIdentifiers[$newPath];
  677.             $uuidMap[$originalUuid] = $newUuid;
  678.             $query 'INSERT INTO phpcr_binarydata (node_id, property_name, workspace_name, idx, data)' '   SELECT ?, b.property_name, ?, b.idx, b.data FROM phpcr_binarydata b WHERE b.node_id = ?';
  679.             try {
  680.                 $this->getConnection()->executeUpdate($query, [$newNodeId$this->workspaceName$row['id']]);
  681.             } catch (DBALException $e) {
  682.                 throw new RepositoryException(
  683.                     "Unexpected exception while copying node from $srcAbsPath to $dstAbsPath",
  684.                     $e->getCode(),
  685.                     $e
  686.                 );
  687.             }
  688.         }
  689.         foreach ($referenceElsToRemap as $data) {
  690.             [$referenceEl$newPath$type$propsData] = $data;
  691.             $referenceEl->nodeValue $uuidMap[$referenceEl->nodeValue];
  692.             $this->syncNode($this->nodeIdentifiers[$newPath], $newPath$typefalse, [], $propsData);
  693.         }
  694.         $this->syncReferences($referencesToUpdate);
  695.     }
  696.     /**
  697.      * @param string $path
  698.      *
  699.      * @return array
  700.      *
  701.      * @throws NamespaceException
  702.      */
  703.     private function getJcrName($path)
  704.     {
  705.         $name implode(''array_slice(explode('/'$path), -11));
  706.         $alias '';
  707.         if (($aliasLength strpos($name':')) !== false) {
  708.             $alias substr($name0$aliasLength);
  709.             $name substr($name$aliasLength 1);
  710.         }
  711.         $namespaces $this->getNamespaces();
  712.         if (!isset($namespaces[$alias])) {
  713.             throw new NamespaceException("the namespace $alias was not registered.");
  714.         }
  715.         return [$namespaces[$alias], $name];
  716.     }
  717.     /**
  718.      * Actually write the node into the database
  719.      *
  720.      * @param string $uuid node uuid
  721.      * @param string $path absolute path of the node
  722.      * @param string $type node type name
  723.      * @param boolean $isNewNode new nodes to insert (true) or existing node to update (false)
  724.      * @param Property[] $props
  725.      * @param array $propsData
  726.      *
  727.      * @return boolean|string
  728.      *
  729.      * @throws ItemExistsException
  730.      * @throws RepositoryException
  731.      * @throws NamespaceException
  732.      * @throws Exception
  733.      */
  734.     private function syncNode($uuid$path$type$isNewNode$props = [], $propsData = [])
  735.     {
  736.         // TODO: Not sure if there are always ALL props in $props, should we grab the online data here?
  737.         // TODO: PERFORMANCE Binary data is handled very inefficiently here, UPSERT will really be necessary here as well as lazy handling
  738.         if (!$propsData) {
  739.             $propsData $this->propsToXML($props);
  740.         }
  741.         if (null === $uuid) {
  742.             $uuid $this->generateUuid();
  743.         }
  744.         if ($isNewNode) {
  745.             [$namespace$localName] = $this->getJcrName($path);
  746.             $qb $this->getConnection()->createQueryBuilder();
  747.             $qb->select(
  748.                 ':identifier, :type, :path, :local_name, :namespace, :parent, :workspace_name, :props, :numerical_props, :depth, COALESCE(MAX(n.sort_order), 0) + 1'
  749.             )->from('phpcr_nodes''n')->where('n.parent = :parent_a');
  750.             $sql $qb->getSQL();
  751.             try {
  752.                 $insert "INSERT INTO phpcr_nodes (identifier, type, path, local_name, namespace, parent, workspace_name, props, numerical_props, depth, sort_order) " $sql;
  753.                 $this->getConnection()->executeUpdate(
  754.                     $insert,
  755.                     $data = [
  756.                         'identifier' => $uuid,
  757.                         'type' => $type,
  758.                         'path' => $path,
  759.                         'local_name' => $localName,
  760.                         'namespace' => $namespace,
  761.                         'parent' => PathHelper::getParentPath($path),
  762.                         'workspace_name' => $this->workspaceName,
  763.                         'props' => $propsData['stringDom']->saveXML(),
  764.                         'numerical_props' => $propsData['numericalDom'] ? $propsData['numericalDom']->saveXML() : null,
  765.                         'depth' => PathHelper::getPathDepth($path),
  766.                         'parent_a' => PathHelper::getParentPath($path),
  767.                     ]
  768.                 );
  769.             } catch (Exception $e) {
  770.                 if ($e instanceof DBALException) {
  771.                     if (strpos($e->getMessage(), 'SQLSTATE[23') !== false) {
  772.                         throw new ItemExistsException('Item ' $path ' already exists in the database');
  773.                     } else {
  774.                         throw new RepositoryException(
  775.                             'Unknown database error while inserting item ' $path ': ' $e->getMessage(),
  776.                             0,
  777.                             $e
  778.                         );
  779.                     }
  780.                 } else {
  781.                     throw $e;
  782.                 }
  783.             }
  784.             $nodeId $this->getConnection()->lastInsertId($this->sequenceNodeName);
  785.         } else {
  786.             $nodeId $this->getSystemIdForNode($path);
  787.             if (!$nodeId) {
  788.                 throw new RepositoryException("nodeId for $path not found");
  789.             }
  790.             $this->getConnection()->update(
  791.                 'phpcr_nodes',
  792.                 [
  793.                     'props' => $propsData['stringDom']->saveXML(),
  794.                     'numerical_props' => $propsData['numericalDom'] ? $propsData['numericalDom']->saveXML() : null,
  795.                 ],
  796.                 ['id' => $nodeId]
  797.             );
  798.         }
  799.         $this->nodeIdentifiers[$path] = $uuid;
  800.         if (!empty($propsData['binaryData'])) {
  801.             $this->syncBinaryData($nodeId$propsData['binaryData']);
  802.         }
  803.         $this->referencesToUpdate[$nodeId] = ['path' => $path'properties' => $propsData['references']];
  804.         return $nodeId;
  805.     }
  806.     private function syncBinaryData(string $nodeId, array $binaryData): void
  807.     {
  808.         $connection $this->getConnection();
  809.         foreach ($binaryData as $propertyName => $binaryValues) {
  810.             foreach ($binaryValues as $idx => $data) {
  811.                 // TODO verify in which cases we can just update
  812.                 $params = [
  813.                     'node_id' => $nodeId,
  814.                     'property_name' => $propertyName,
  815.                     'workspace_name' => $this->workspaceName,
  816.                 ];
  817.                 $connection->delete('phpcr_binarydata'$params);
  818.                 $params['idx'] = $idx;
  819.                 $params['data'] = $data;
  820.                 $types = [
  821.                     ParameterType::INTEGER,
  822.                     ParameterType::STRING,
  823.                     ParameterType::STRING,
  824.                     ParameterType::INTEGER,
  825.                     ParameterType::LARGE_OBJECT,
  826.                 ];
  827.                 $connection->insert('phpcr_binarydata'$params$types);
  828.             }
  829.         }
  830.     }
  831.     /**
  832.      * @param array $referencesToUpdate
  833.      *
  834.      * @throws RepositoryException
  835.      * @throws ReferentialIntegrityException
  836.      */
  837.     private function syncReferences(array $referencesToUpdate)
  838.     {
  839.         if ($referencesToUpdate) {
  840.             // do not update references that are going to be deleted anyways
  841.             $toUpdate array_diff(array_keys($referencesToUpdate), array_keys($this->referencesToDelete));
  842.             try {
  843.                 foreach ($this->referenceTables as $table) {
  844.                     $query "DELETE FROM $table WHERE source_id IN (?)";
  845.                     $this->executeChunkedUpdate($query$toUpdate);
  846.                 }
  847.             } catch (DBALException $e) {
  848.                 throw new RepositoryException('Unexpected exception while cleaning up after saving'$e->getCode(), $e);
  849.             }
  850.             $updates = [];
  851.             foreach ($toUpdate as $nodeId) {
  852.                 $references $referencesToUpdate[$nodeId];
  853.                 foreach ($references['properties'] as $name => $data) {
  854.                     foreach ($data['values'] as $value) {
  855.                         $targetId $this->getSystemIdForNode($value);
  856.                         if (false === $targetId) {
  857.                             if (PropertyType::REFERENCE === $data['type']) {
  858.                                 throw new ReferentialIntegrityException(
  859.                                     sprintf(
  860.                                         'Trying to store reference to non-existant node with path "%s" in node "%s" "%s"',
  861.                                         $value,
  862.                                         $references['path'],
  863.                                         $name
  864.                                     )
  865.                                 );
  866.                             }
  867.                             continue;
  868.                         }
  869.                         $key $targetId '-' $nodeId '-' $name;
  870.                         // it is valid to have multiple references to the same node in a multivalue
  871.                         // but it is not desired to store duplicates in the database
  872.                         $updates[$key] = [
  873.                             'type' => $data['type'],
  874.                             'data' => [
  875.                                 'source_id' => $nodeId,
  876.                                 'source_property_name' => $name,
  877.                                 'target_id' => $targetId,
  878.                             ],
  879.                         ];
  880.                     }
  881.                 }
  882.             }
  883.             foreach ($updates as $update) {
  884.                 $this->getConnection()->insert($this->referenceTables[$update['type']], $update['data']);
  885.             }
  886.         }
  887.         // TODO on RDBMS that support deferred FKs we could skip this step
  888.         if ($this->referencesToDelete) {
  889.             $params array_keys($this->referencesToDelete);
  890.             // remove all PropertyType::REFERENCE with a source_id on a deleted node
  891.             try {
  892.                 $query "DELETE FROM phpcr_nodes_references WHERE source_id IN (?)";
  893.                 $this->executeChunkedUpdate($query$params);
  894.             } catch (DBALException $e) {
  895.                 throw new RepositoryException(
  896.                     'Unexpected exception while cleaning up deleted nodes',
  897.                     $e->getCode(),
  898.                     $e
  899.                 );
  900.             }
  901.             // ensure that there are no PropertyType::REFERENCE pointing to nodes that will be deleted
  902.             // Note: due to the outer join we cannot filter on workspace_name, but this is ok
  903.             // since within a transaction there can never be missing referenced nodes within the current workspace
  904.             // make sure the target node is not in the list of nodes being deleted, to allow deletion in same request
  905.             $query 'SELECT DISTINCT r.target_id
  906.             FROM phpcr_nodes_references r
  907.                 LEFT OUTER JOIN phpcr_nodes n ON r.target_id = n.id
  908.             WHERE r.target_id IN (?)';
  909.             if ($this->getConnection()->getDatabasePlatform() instanceof SqlitePlatform) {
  910.                 $missingTargets = [];
  911.                 foreach (array_chunk($paramsself::SQLITE_MAXIMUM_IN_PARAM_COUNT) as $chunk) {
  912.                     $stmt $this->getConnection()->executeQuery($query, [$chunk], [Connection::PARAM_INT_ARRAY]);
  913.                     $missingTargets array_merge($missingTargetsarray_column($stmt->fetchAllNumeric(), 0));
  914.                 }
  915.             } else {
  916.                 $stmt $this->getConnection()->executeQuery($query, [$params], [Connection::PARAM_INT_ARRAY]);
  917.                 $missingTargets array_column($stmt->fetchAllNumeric(), 0);
  918.             }
  919.             if ($missingTargets) {
  920.                 $paths = [];
  921.                 foreach ($missingTargets as $id) {
  922.                     if (isset($this->referencesToDelete[$id])) {
  923.                         $paths[] = $this->referencesToDelete[$id];
  924.                     }
  925.                 }
  926.                 throw new ReferentialIntegrityException(
  927.                     "Cannot delete '" implode("', '"$paths) . "': A reference points to this node or a subnode"
  928.                 );
  929.             }
  930.             // clean up all references
  931.             try {
  932.                 foreach ($this->referenceTables as $table) {
  933.                     $query "DELETE FROM $table WHERE target_id IN (?)";
  934.                     $this->executeChunkedUpdate($query$params);
  935.                 }
  936.             } catch (DBALException $e) {
  937.                 throw new RepositoryException(
  938.                     'Unexpected exception while cleaning up deleted nodes',
  939.                     $e->getCode(),
  940.                     $e
  941.                 );
  942.             }
  943.         }
  944.     }
  945.     /**
  946.      * Convert the node XML to stdClass node representation containing all properties
  947.      *
  948.      * @param string $xml
  949.      *
  950.      * @return stdClass
  951.      *
  952.      * @throws InvalidArgumentException
  953.      */
  954.     protected function xmlToProps($xml)
  955.     {
  956.         $xmlParser = new XmlToPropsParser(
  957.             $xml,
  958.             $this->valueConverter
  959.         );
  960.         return $xmlParser->parse();
  961.     }
  962.     /**
  963.      * Convert the node XML to stdClass node representation containing only the given properties
  964.      *
  965.      * @param string $xml
  966.      * @param array $propertyNames
  967.      *
  968.      * @return stdClass
  969.      *
  970.      * @throws InvalidArgumentException
  971.      */
  972.     protected function xmlToColumns($xml$propertyNames)
  973.     {
  974.         $xmlParser = new XmlToPropsParser(
  975.             $xml,
  976.             $this->valueConverter,
  977.             $propertyNames
  978.         );
  979.         return $xmlParser->parse();
  980.     }
  981.     /**
  982.      * Map a property from the given property XML node to the given \stdClass node representation
  983.      *
  984.      * @param DOMElement $propertyNode
  985.      * @param stdClass $data
  986.      *
  987.      * @throws InvalidArgumentException
  988.      *
  989.      * @deprecated This method was replaced with the `XmlToPropsParser` for performance reasons.
  990.      */
  991.     protected function mapPropertyFromElement(DOMElement $propertyNodestdClass $data)
  992.     {
  993.         @trigger_error(
  994.             sprintf('The "%s" method is deprecated since jackalope/jackalope-doctrine-dbal 1.7.'__METHOD__),
  995.             \E_USER_DEPRECATED
  996.         );
  997.         $name $propertyNode->getAttribute('sv:name');
  998.         $values = [];
  999.         $type PropertyType::valueFromName($propertyNode->getAttribute('sv:type'));
  1000.         foreach ($propertyNode->childNodes as $valueNode) {
  1001.             switch ($type) {
  1002.                 case PropertyType::NAME:
  1003.                 case PropertyType::URI:
  1004.                 case PropertyType::WEAKREFERENCE:
  1005.                 case PropertyType::REFERENCE:
  1006.                 case PropertyType::PATH:
  1007.                 case PropertyType::DECIMAL:
  1008.                 case PropertyType::STRING:
  1009.                     $values[] = $valueNode->nodeValue;
  1010.                     break;
  1011.                 case PropertyType::BOOLEAN:
  1012.                     $values[] = (bool)$valueNode->nodeValue;
  1013.                     break;
  1014.                 case PropertyType::LONG:
  1015.                     $values[] = (int)$valueNode->nodeValue;
  1016.                     break;
  1017.                 case PropertyType::BINARY:
  1018.                     $values[] = (int)$valueNode->nodeValue;
  1019.                     break;
  1020.                 case PropertyType::DATE:
  1021.                     $date $valueNode->nodeValue;
  1022.                     if ($date) {
  1023.                         $date = new DateTime($date);
  1024.                         $date->setTimezone(new DateTimeZone(date_default_timezone_get()));
  1025.                         // Jackalope expects a string, might make sense to refactor to allow DateTime instances too
  1026.                         $date $this->valueConverter->convertType($datePropertyType::STRING);
  1027.                     }
  1028.                     $values[] = $date;
  1029.                     break;
  1030.                 case PropertyType::DOUBLE:
  1031.                     $values[] = (double)$valueNode->nodeValue;
  1032.                     break;
  1033.                 default:
  1034.                     throw new InvalidArgumentException("Type with constant $type not found.");
  1035.             }
  1036.         }
  1037.         switch ($type) {
  1038.             case PropertyType::BINARY:
  1039.                 $data->{':' $name} = $propertyNode->getAttribute('sv:multi-valued') ? $values $values[0];
  1040.                 break;
  1041.             default:
  1042.                 $data->{$name} = $propertyNode->getAttribute('sv:multi-valued') ? $values $values[0];
  1043.                 $data->{':' $name} = $type;
  1044.                 break;
  1045.         }
  1046.     }
  1047.     /**
  1048.      * Seperate properties array into an xml and binary data.
  1049.      *
  1050.      * @param array $properties
  1051.      *
  1052.      * @return array (
  1053.      *     'stringDom' => $stringDom,
  1054.      *     'numericalDom' => $numericalDom',
  1055.      *     'binaryData' => streams,
  1056.      *     'references' => ['type' => INT, 'values' => [UUIDs])
  1057.      * )
  1058.      *
  1059.      * @throws RepositoryException
  1060.      */
  1061.     private function propsToXML($properties)
  1062.     {
  1063.         $namespaces = [
  1064.             NS::PREFIX_MIX => NS::NAMESPACE_MIX,
  1065.             NS::PREFIX_NT => NS::NAMESPACE_NT,
  1066.             'xs' => 'http://www.w3.org/2001/XMLSchema',
  1067.             NS::PREFIX_JCR => NS::NAMESPACE_JCR,
  1068.             NS::PREFIX_SV => NS::NAMESPACE_SV,
  1069.             'rep' => 'internal',
  1070.         ];
  1071.         $doms = [
  1072.             'stringDom' => [],
  1073.             'numericalDom' => [],
  1074.         ];
  1075.         $binaryData $references = [];
  1076.         foreach ($properties as $property) {
  1077.             $targetDoms = ['stringDom'];
  1078.             switch ($property->getType()) {
  1079.                 case PropertyType::WEAKREFERENCE:
  1080.                 case PropertyType::REFERENCE:
  1081.                     $references[$property->getName()] = [
  1082.                         'type' => $property->getType(),
  1083.                         'values' => $property->isMultiple() ? $property->getString() : [$property->getString()],
  1084.                     ];
  1085.                     // no break
  1086.                 case PropertyType::NAME:
  1087.                 case PropertyType::URI:
  1088.                 case PropertyType::PATH:
  1089.                 case PropertyType::STRING:
  1090.                     $values $property->getString();
  1091.                     break;
  1092.                 case PropertyType::DECIMAL:
  1093.                     $values $property->getDecimal();
  1094.                     $targetDoms[] = 'numericalDom';
  1095.                     break;
  1096.                 case PropertyType::BOOLEAN:
  1097.                     $values array_map('intval', (array)$property->getBoolean());
  1098.                     break;
  1099.                 case PropertyType::LONG:
  1100.                     $values $property->getLong();
  1101.                     $targetDoms[] = 'numericalDom';
  1102.                     break;
  1103.                 case PropertyType::BINARY:
  1104.                     if ($property->isNew() || $property->isModified()) {
  1105.                         $values = [];
  1106.                         foreach ((array)$property->getValueForStorage() as $stream) {
  1107.                             if (null === $stream) {
  1108.                                 $binary '';
  1109.                             } else {
  1110.                                 $binary stream_get_contents($stream);
  1111.                                 fclose($stream);
  1112.                             }
  1113.                             $binaryData[$property->getName()][] = $binary;
  1114.                             $length strlen($binary);
  1115.                             $values[] = $length;
  1116.                         }
  1117.                     } else {
  1118.                         $values $property->getLength();
  1119.                         if (!$property->isMultiple() && empty($values)) {
  1120.                             $values = [0];
  1121.                         }
  1122.                     }
  1123.                     break;
  1124.                 case PropertyType::DATE:
  1125.                     $values $property->getDate();
  1126.                     if ($values instanceof DateTime) {
  1127.                         $values = [$values];
  1128.                     }
  1129.                     foreach ((array)$values as $key => $date) {
  1130.                         if ($date instanceof DateTime) {
  1131.                             // do not modify the instance which is associated with the node.
  1132.                             $date = clone $date;
  1133.                             // normalize to UTC for storage.
  1134.                             $date->setTimezone(new DateTimeZone('UTC'));
  1135.                         }
  1136.                         $values[$key] = $date;
  1137.                     }
  1138.                     $values $this->valueConverter->convertType($valuesPropertyType::STRING);
  1139.                     break;
  1140.                 case PropertyType::DOUBLE:
  1141.                     $values $property->getDouble();
  1142.                     $targetDoms[] = 'numericalDom';
  1143.                     break;
  1144.                 default:
  1145.                     throw new RepositoryException('unknown type ' $property->getType());
  1146.             }
  1147.             foreach ($targetDoms as $targetDom) {
  1148.                 $doms[$targetDom][] = [
  1149.                     'name' => $property->getName(),
  1150.                     'type' => PropertyType::nameFromValue($property->getType()),
  1151.                     'multiple' => $property->isMultiple(),
  1152.                     'lengths' => (array)$property->getLength(),
  1153.                     'values' => $values,
  1154.                 ];
  1155.             }
  1156.         }
  1157.         $ret = [
  1158.             'stringDom' => null,
  1159.             'numericalDom' => null,
  1160.             'binaryData' => $binaryData,
  1161.             'references' => $references
  1162.         ];
  1163.         foreach ($doms as $targetDom => $properties) {
  1164.             $dom = new DOMDocument('1.0''UTF-8');
  1165.             $rootNode $dom->createElement('sv:node');
  1166.             foreach ($namespaces as $namespace => $uri) {
  1167.                 $rootNode->setAttribute('xmlns:' $namespace$uri);
  1168.             }
  1169.             $dom->appendChild($rootNode);
  1170.             foreach ($properties as $property) {
  1171.                 /* @var $property Property */
  1172.                 $propertyNode $dom->createElement('sv:property');
  1173.                 $propertyNode->setAttribute('sv:name'$property['name']);
  1174.                 $propertyNode->setAttribute('sv:type'$property['type']);
  1175.                 $propertyNode->setAttribute('sv:multi-valued'$property['multiple'] ? '1' '0');
  1176.                 $lengths = (array)$property['lengths'];
  1177.                 foreach ((array)$property['values'] as $key => $value) {
  1178.                     $element $propertyNode->appendChild($dom->createElement('sv:value'));
  1179.                     $element->appendChild($dom->createTextNode($value));
  1180.                     if (isset($lengths[$key])) {
  1181.                         $lengthAttribute $dom->createAttribute('length');
  1182.                         $lengthAttribute->value $lengths[$key];
  1183.                         $element->appendChild($lengthAttribute);
  1184.                     }
  1185.                 }
  1186.                 $rootNode->appendChild($propertyNode);
  1187.             }
  1188.             if (count($properties)) {
  1189.                 $ret[$targetDom] = $dom;
  1190.             }
  1191.         }
  1192.         return $ret;
  1193.     }
  1194.     /**
  1195.      * {@inheritDoc}
  1196.      *
  1197.      * @throws DBALException
  1198.      */
  1199.     public function getAccessibleWorkspaceNames()
  1200.     {
  1201.         $query "SELECT DISTINCT name FROM phpcr_workspaces";
  1202.         $stmt $this->getConnection()->executeQuery($query);
  1203.         return array_column($stmt->fetchAllNumeric(), 0);
  1204.     }
  1205.     /**
  1206.      * {@inheritDoc}
  1207.      *
  1208.      * @throws RepositoryException
  1209.      * @throws DBALException
  1210.      */
  1211.     public function getNode($path)
  1212.     {
  1213.         $this->assertLoggedIn();
  1214.         PathHelper::assertValidAbsolutePath($pathfalsetrue$this->getNamespacePrefixes());
  1215.         $values['path'] = $path;
  1216.         $values['pathd'] = rtrim($path'/') . '/%';
  1217.         $values['workspace'] = $this->workspaceName;
  1218.         if ($this->fetchDepth 0) {
  1219.             $values['fetchDepth'] = $this->fetchDepth;
  1220.             $query '
  1221.               SELECT * FROM phpcr_nodes
  1222.               WHERE (path LIKE :pathd OR path = :path)
  1223.                 AND workspace_name = :workspace
  1224.                 AND depth <= ((SELECT depth FROM phpcr_nodes WHERE path = :path AND workspace_name = :workspace) + :fetchDepth)
  1225.               ORDER BY depth, sort_order ASC';
  1226.         } else {
  1227.             $query '
  1228.               SELECT * FROM phpcr_nodes
  1229.               WHERE path = :path
  1230.                 AND workspace_name = :workspace
  1231.               ORDER BY depth, sort_order ASC';
  1232.         }
  1233.         $stmt $this->getConnection()->executeQuery($query$values);
  1234.         $rows $stmt->fetchAllAssociative();
  1235.         if (empty($rows)) {
  1236.             throw new ItemNotFoundException("Item $path not found in workspace " $this->workspaceName);
  1237.         }
  1238.         $nestedNodes $this->getNodesData($rows);
  1239.         $node array_shift($nestedNodes);
  1240.         foreach ($nestedNodes as $nestedPath => $nested) {
  1241.             $relativePath PathHelper::relativizePath($nestedPath$path);
  1242.             $this->nestNode($node$nestedexplode('/'$relativePath));
  1243.         }
  1244.         return $node;
  1245.     }
  1246.     /**
  1247.      * Attach a node at a subpath under the ancestor node.
  1248.      *
  1249.      * @param stdClass $ancestor The root node
  1250.      * @param stdClass $node The node to add
  1251.      * @param array $nodeNames Breadcrumb of child nodes from parentNode to the node itself
  1252.      */
  1253.     private function nestNode($ancestor$node, array $nodeNames)
  1254.     {
  1255.         while ($name array_shift($nodeNames)) {
  1256.             if (empty($nodeNames)) {
  1257.                 $ancestor->{$name} = $node;
  1258.                 return;
  1259.             }
  1260.             $ancestor $ancestor->{$name};
  1261.         }
  1262.     }
  1263.     /**
  1264.      * Convert a node result row to the stdClass representing all raw data.
  1265.      *
  1266.      * @param array $row
  1267.      *
  1268.      * @return stdClass raw node data
  1269.      */
  1270.     private function getNodeData($row)
  1271.     {
  1272.         $data $this->getNodesData([$row]);
  1273.         return array_shift($data);
  1274.     }
  1275.     /**
  1276.      * Build the raw data for a list of database result rows, fetching the
  1277.      * additional information in one single query.
  1278.      *
  1279.      * @param array $rows
  1280.      *
  1281.      * @return stdClass[]
  1282.      *
  1283.      * @throws NoSuchNodeTypeException
  1284.      * @throws RepositoryException
  1285.      */
  1286.     private function getNodesData($rows)
  1287.     {
  1288.         $data = [];
  1289.         $paths = [];
  1290.         foreach ($rows as $row) {
  1291.             $this->nodeIdentifiers[$row['path']] = $row['identifier'];
  1292.             $data[$row['path']] = $this->xmlToProps($row['props']);
  1293.             $data[$row['path']]->{'jcr:primaryType'} = $row['type'];
  1294.             $paths[] = $row['path'];
  1295.         }
  1296.         $query 'SELECT path, parent FROM phpcr_nodes WHERE parent IN (?) AND workspace_name = ? ORDER BY sort_order ASC';
  1297.         if ($this->getConnection()->getDatabasePlatform() instanceof SqlitePlatform) {
  1298.             $childrenRows = [];
  1299.             foreach (array_chunk($pathsself::SQLITE_MAXIMUM_IN_PARAM_COUNT) as $chunk) {
  1300.                 $childrenRows += $this->getConnection()->fetchAllAssociative(
  1301.                     $query,
  1302.                     [$chunk$this->workspaceName],
  1303.                     [Connection::PARAM_STR_ARRAYnull]
  1304.                 );
  1305.             }
  1306.         } else {
  1307.             $childrenRows $this->getConnection()->fetchAllAssociative(
  1308.                 $query,
  1309.                 [$paths$this->workspaceName],
  1310.                 [Connection::PARAM_STR_ARRAYnull]
  1311.             );
  1312.         }
  1313.         foreach ($childrenRows as $child) {
  1314.             $childName explode('/'$child['path']);
  1315.             $childName end($childName);
  1316.             if (!isset($data[$child['parent']]->{$childName})) {
  1317.                 $data[$child['parent']]->{$childName} = new stdClass();
  1318.             }
  1319.         }
  1320.         foreach (array_keys($data) as $path) {
  1321.             // If the node is referenceable, return jcr:uuid.
  1322.             if (isset($data[$path]->{'jcr:mixinTypes'})) {
  1323.                 foreach ((array)$data[$path]->{'jcr:mixinTypes'} as $mixin) {
  1324.                     if ($this->nodeTypeManager->getNodeType($mixin)->isNodeType('mix:referenceable')) {
  1325.                         $data[$path]->{'jcr:uuid'} = $this->nodeIdentifiers[$path];
  1326.                         break;
  1327.                     }
  1328.                 }
  1329.             }
  1330.         }
  1331.         return $data;
  1332.     }
  1333.     /**
  1334.      * {@inheritDoc}
  1335.      *
  1336.      * @throws RepositoryException
  1337.      */
  1338.     public function getNodes($paths)
  1339.     {
  1340.         $this->assertLoggedIn();
  1341.         if (empty($paths)) {
  1342.             return [];
  1343.         }
  1344.         foreach ($paths as $path) {
  1345.             PathHelper::assertValidAbsolutePath($pathfalsetrue$this->getNamespacePrefixes());
  1346.         }
  1347.         $params['workspace'] = $this->workspaceName;
  1348.         if ($this->fetchDepth 0) {
  1349.             $params['fetchDepth'] = $this->fetchDepth;
  1350.             $query '
  1351.               SELECT path AS arraykey, id, path, parent, local_name, namespace, workspace_name, identifier, type, props, depth, sort_order
  1352.               FROM phpcr_nodes
  1353.               WHERE workspace_name = :workspace
  1354.                 AND (';
  1355.             $i 0;
  1356.             foreach ($paths as $path) {
  1357.                 $params['path' $i] = $path;
  1358.                 $params['pathd' $i] = rtrim($path'/') . '/%';
  1359.                 $subquery 'SELECT depth FROM phpcr_nodes WHERE path = :path' $i ' AND workspace_name = :workspace';
  1360.                 $query .= '(path LIKE :pathd' $i ' OR path = :path' $i ') AND depth <= ((' $subquery ') + :fetchDepth) OR ';
  1361.                 $i++;
  1362.             }
  1363.         } else {
  1364.             $query 'SELECT path AS arraykey, id, path, parent, local_name, namespace, workspace_name, identifier, type, props, depth, sort_order
  1365.                 FROM phpcr_nodes WHERE workspace_name = :workspace AND (';
  1366.             $i 0;
  1367.             foreach ($paths as $path) {
  1368.                 $params['path' $i] = $path;
  1369.                 $query .= 'path = :path' $i ' OR ';
  1370.                 $i++;
  1371.             }
  1372.         }
  1373.         $query rtrim($query'OR ');
  1374.         $query .= ') ORDER BY sort_order ASC';
  1375.         $stmt $this->getConnection()->executeQuery($query$params);
  1376.         // emulate old $stmt->fetchAll(PDO::FETCH_UNIQUE | PDO::FETCH_GROUP)
  1377.         $all = [];
  1378.         while ($row $stmt->fetchAssociative()) {
  1379.             $index array_shift($row);
  1380.             $all[$index] = $row;
  1381.         }
  1382.         $nodes = [];
  1383.         if ($all) {
  1384.             $nodes $this->getNodesData($all);
  1385.         }
  1386.         return $nodes;
  1387.     }
  1388.     /**
  1389.      * Determine if a path exists already.
  1390.      *
  1391.      * @param string $path Path of the node
  1392.      * @param string $workspaceName To overwrite the current workspace
  1393.      *
  1394.      * @return bool
  1395.      */
  1396.     private function pathExists($path$workspaceName null)
  1397.     {
  1398.         return (boolean)$this->getSystemIdForNode($path$workspaceName);
  1399.     }
  1400.     /**
  1401.      * Get the database primary key for node.
  1402.      *
  1403.      * @param string $identifier Path of the identifier
  1404.      * @param string $workspaceName To overwrite the current workspace
  1405.      *
  1406.      * @return bool|string The database id
  1407.      */
  1408.     private function getSystemIdForNode($identifier$workspaceName null)
  1409.     {
  1410.         if (null === $workspaceName) {
  1411.             $workspaceName $this->workspaceName;
  1412.         }
  1413.         if (UUIDHelper::isUUID($identifier)) {
  1414.             $query 'SELECT id FROM phpcr_nodes WHERE identifier = ? AND workspace_name = ?';
  1415.         } else {
  1416.             $platform $this->getConnection()->getDatabasePlatform();
  1417.             if ($platform instanceof MySQLPlatform) {
  1418.                 $query 'SELECT id FROM phpcr_nodes WHERE path COLLATE ' $this->getCaseSensitiveEncoding() . ' = ? AND workspace_name = ?';
  1419.             } else {
  1420.                 $query 'SELECT id FROM phpcr_nodes WHERE path = ? AND workspace_name = ?';
  1421.             }
  1422.         }
  1423.         $nodeId $this->getConnection()->fetchOne($query, [$identifier$workspaceName]);
  1424.         return $nodeId ?: false;
  1425.     }
  1426.     /**
  1427.      * {@inheritDoc}
  1428.      */
  1429.     public function getNodeByIdentifier($uuid)
  1430.     {
  1431.         $this->assertLoggedIn();
  1432.         $query 'SELECT * FROM phpcr_nodes WHERE identifier = ? AND workspace_name = ?';
  1433.         $row $this->getConnection()->fetchAssociative($query, [$uuid$this->workspaceName]);
  1434.         if (!$row) {
  1435.             throw new ItemNotFoundException("Item $uuid not found in workspace " $this->workspaceName);
  1436.         }
  1437.         $path $row['path'];
  1438.         $data $this->getNodeData($row);
  1439.         $data->{':jcr:path'} = $path;
  1440.         return $data;
  1441.     }
  1442.     /**
  1443.      * {@inheritDoc}
  1444.      */
  1445.     public function getNodesByIdentifier($identifiers)
  1446.     {
  1447.         $this->assertLoggedIn();
  1448.         if (empty($identifiers)) {
  1449.             return [];
  1450.         }
  1451.         $query 'SELECT id, path, parent, local_name, namespace, workspace_name, identifier, type, props, depth, sort_order
  1452.             FROM phpcr_nodes WHERE workspace_name = ? AND identifier IN (?)';
  1453.         if ($this->getConnection()->getDatabasePlatform() instanceof SqlitePlatform) {
  1454.             $all = [];
  1455.             foreach (array_chunk($identifiersself::SQLITE_MAXIMUM_IN_PARAM_COUNT) as $chunk) {
  1456.                 $all += $this->getConnection()->fetchAllAssociative(
  1457.                     $query,
  1458.                     [$this->workspaceName$chunk],
  1459.                     [ParameterType::STRINGConnection::PARAM_STR_ARRAY]
  1460.                 );
  1461.             }
  1462.         } else {
  1463.             $all $this->getConnection()->fetchAllAssociative(
  1464.                 $query,
  1465.                 [$this->workspaceName$identifiers],
  1466.                 [ParameterType::STRINGConnection::PARAM_STR_ARRAY]
  1467.             );
  1468.         }
  1469.         $nodes = [];
  1470.         if ($all) {
  1471.             $nodesData $this->getNodesData($all);
  1472.             // ensure that the nodes are returned in the order if how the identifiers were passed in
  1473.             $pathByUuid = [];
  1474.             foreach ($nodesData as $path => $node) {
  1475.                 $pathByUuid[$node->{'jcr:uuid'}] = $path;
  1476.             }
  1477.             foreach ($identifiers as $identifier) {
  1478.                 if (isset($pathByUuid[$identifier])) {
  1479.                     $nodes[$pathByUuid[$identifier]] = $nodesData[$pathByUuid[$identifier]];
  1480.                 }
  1481.             }
  1482.         }
  1483.         return $nodes;
  1484.     }
  1485.     /**
  1486.      * {@inheritDoc}
  1487.      *
  1488.      * @throws NotImplementedException
  1489.      * @throws ItemNotFoundException
  1490.      */
  1491.     public function getNodePathForIdentifier($uuid$workspace null)
  1492.     {
  1493.         if (null !== $workspace) {
  1494.             throw new NotImplementedException('Specifying the workspace is not yet supported.');
  1495.         }
  1496.         $this->assertLoggedIn();
  1497.         $query "SELECT path FROM phpcr_nodes WHERE identifier = ? AND workspace_name = ?";
  1498.         $path $this->getConnection()->fetchOne($query, [$uuid$this->workspaceName]);
  1499.         if (!$path) {
  1500.             throw new ItemNotFoundException("no item found with uuid " $uuid);
  1501.         }
  1502.         return $path;
  1503.     }
  1504.     /**
  1505.      * {@inheritDoc}
  1506.      */
  1507.     public function deleteNodes(array $operations)
  1508.     {
  1509.         $this->assertLoggedIn();
  1510.         foreach ($operations as $op) {
  1511.             $this->deleteNode($op->srcPath);
  1512.         }
  1513.         return true;
  1514.     }
  1515.     /**
  1516.      * {@inheritDoc}
  1517.      */
  1518.     public function deleteNodeImmediately($path)
  1519.     {
  1520.         $this->prepareSave();
  1521.         $this->deleteNode($path);
  1522.         $this->finishSave();
  1523.         return true;
  1524.     }
  1525.     /**
  1526.      * TODO instead of calling the deletes separately, we should batch the delete query
  1527.      * but careful with the caching!
  1528.      *
  1529.      * @param string $path node path to delete
  1530.      *
  1531.      * @throws ConstraintViolationException
  1532.      * @throws ItemNotFoundException
  1533.      * @throws RepositoryException
  1534.      */
  1535.     protected function deleteNode($path)
  1536.     {
  1537.         if ('/' === $path) {
  1538.             throw new ConstraintViolationException('You can not delete the root node of a repository');
  1539.         }
  1540.         if (!$this->pathExists($path)) {
  1541.             throw new ItemNotFoundException("No node found at " $path);
  1542.         }
  1543.         $params = [$path$path "/%"$this->workspaceName];
  1544.         // TODO on RDBMS that support deferred FKs we could skip this step
  1545.         $query 'SELECT id, path FROM phpcr_nodes WHERE (path = ? OR path LIKE ?) AND workspace_name = ?';
  1546.         $stmt $this->getConnection()->executeQuery($query$params);
  1547.         $this->referencesToDelete += array_column($stmt->fetchAllNumeric(), 10);
  1548.         try {
  1549.             $query 'DELETE FROM phpcr_nodes WHERE (path = ? OR path LIKE ?) AND workspace_name = ?';
  1550.             $this->getConnection()->executeUpdate($query$params);
  1551.             $this->cleanIdentifierCache($path);
  1552.         } catch (DBALException $e) {
  1553.             throw new RepositoryException('Unexpected exception while deleting node ' $path$e->getCode(), $e);
  1554.         }
  1555.     }
  1556.     /**
  1557.      * Clean all identifiers under path $root.
  1558.      *
  1559.      * @param string $root Path to the root node to be cleared
  1560.      */
  1561.     private function cleanIdentifierCache($root)
  1562.     {
  1563.         unset($this->nodeIdentifiers[$root]);
  1564.         foreach (array_keys($this->nodeIdentifiers) as $path) {
  1565.             if (strpos($path"$root/") === 0) {
  1566.                 unset($this->nodeIdentifiers[$path]);
  1567.             }
  1568.         }
  1569.     }
  1570.     /**
  1571.      * {@inheritDoc}
  1572.      */
  1573.     public function deleteProperties(array $operations)
  1574.     {
  1575.         $this->assertLoggedIn();
  1576.         foreach ($operations as $op) {
  1577.             $this->deleteProperty($op->srcPath);
  1578.         }
  1579.         return true;
  1580.     }
  1581.     /**
  1582.      * {@inheritDoc}
  1583.      */
  1584.     public function deletePropertyImmediately($path)
  1585.     {
  1586.         $this->prepareSave();
  1587.         $this->deleteProperty($path);
  1588.         $this->finishSave();
  1589.         return true;
  1590.     }
  1591.     /**
  1592.      * {@inheritDoc}
  1593.      *
  1594.      * @throws ItemNotFoundException
  1595.      * @throws RepositoryException
  1596.      */
  1597.     protected function deleteProperty($path)
  1598.     {
  1599.         $this->assertLoggedIn();
  1600.         $nodePath PathHelper::getParentPath($path);
  1601.         $nodeId $this->getSystemIdForNode($nodePath);
  1602.         if (!$nodeId) {
  1603.             // no we really don't know that path
  1604.             throw new ItemNotFoundException("No item found at " $path);
  1605.         }
  1606.         $query 'SELECT props FROM phpcr_nodes WHERE id = ?';
  1607.         $xml $this->getConnection()->fetchOne($query, [$nodeId]);
  1608.         $dom = new DOMDocument('1.0''UTF-8');
  1609.         $dom->loadXML($xml);
  1610.         $xpath = new DomXpath($dom);
  1611.         $found false;
  1612.         $propertyName PathHelper::getNodeName($path);
  1613.         foreach ($xpath->query(sprintf('//*[@sv:name="%s"]'$propertyName)) as $propertyNode) {
  1614.             $found true;
  1615.             // would be nice to have the property object to ask for type
  1616.             // but its in state deleted, would mean lots of refactoring
  1617.             if ($propertyNode->hasAttribute('sv:type')) {
  1618.                 $type strtolower($propertyNode->getAttribute('sv:type'));
  1619.                 if (in_array($type, ['reference''weakreference'])) {
  1620.                     $table $this->referenceTables['reference' === $type PropertyType::REFERENCE PropertyType::WEAKREFERENCE];
  1621.                     try {
  1622.                         $query "DELETE FROM $table WHERE source_id = ? AND source_property_name = ?";
  1623.                         $this->getConnection()->executeUpdate($query, [$nodeId$propertyName]);
  1624.                     } catch (DBALException $e) {
  1625.                         throw new RepositoryException(
  1626.                             'Unexpected exception while cleaning up deleted nodes',
  1627.                             $e->getCode(),
  1628.                             $e
  1629.                         );
  1630.                     }
  1631.                 }
  1632.             }
  1633.             $propertyNode->parentNode->removeChild($propertyNode);
  1634.         }
  1635.         if (!$found) {
  1636.             throw new ItemNotFoundException("Node $nodePath has no property $propertyName");
  1637.         }
  1638.         $xml $dom->saveXML();
  1639.         $query 'UPDATE phpcr_nodes SET props = ? WHERE id = ?';
  1640.         $params = [$xml$nodeId];
  1641.         try {
  1642.             $this->getConnection()->executeUpdate($query$params);
  1643.         } catch (DBALException $e) {
  1644.             throw new RepositoryException("Unexpected exception while updating properties of $path"$e->getCode(), $e);
  1645.         }
  1646.     }
  1647.     /**
  1648.      * {@inheritDoc}
  1649.      */
  1650.     public function moveNodes(array $operations)
  1651.     {
  1652.         /** @var $op MoveNodeOperation */
  1653.         foreach ($operations as $op) {
  1654.             $this->moveNode($op->srcPath$op->dstPath);
  1655.         }
  1656.         return true;
  1657.     }
  1658.     /**
  1659.      * {@inheritDoc}
  1660.      */
  1661.     public function moveNodeImmediately($srcAbsPath$dstAbspath)
  1662.     {
  1663.         $this->prepareSave();
  1664.         $this->moveNode($srcAbsPath$dstAbspath);
  1665.         $this->finishSave();
  1666.         return true;
  1667.     }
  1668.     /**
  1669.      * Execute moving a single node
  1670.      *
  1671.      * @throws PathNotFoundException
  1672.      * @throws ItemExistsException
  1673.      * @throws RepositoryException
  1674.      */
  1675.     protected function moveNode($srcAbsPath$dstAbsPath)
  1676.     {
  1677.         $this->assertLoggedIn();
  1678.         PathHelper::assertValidAbsolutePath($srcAbsPathfalsetrue$this->getNamespacePrefixes());
  1679.         PathHelper::assertValidAbsolutePath($dstAbsPathtruetrue$this->getNamespacePrefixes());
  1680.         if (!$this->pathExists($srcAbsPath)) {
  1681.             throw new PathNotFoundException("Source path '$srcAbsPath' not found");
  1682.         }
  1683.         if ($this->getSystemIdForNode($dstAbsPath)) {
  1684.             throw new ItemExistsException(
  1685.                 "Cannot move '$srcAbsPath' to '$dstAbsPath' because destination node already exists."
  1686.             );
  1687.         }
  1688.         if (!$this->getSystemIdForNode(PathHelper::getParentPath($dstAbsPath))) {
  1689.             throw new PathNotFoundException("Parent of the destination path '" $dstAbsPath "' has to exist.");
  1690.         }
  1691.         $query 'SELECT path, id FROM phpcr_nodes WHERE path LIKE ? OR path = ? AND workspace_name = ? ' $this->getConnection(
  1692.             )->getDatabasePlatform()->getForUpdateSQL();
  1693.         $stmt $this->getConnection()->executeQuery($query, [$srcAbsPath '/%'$srcAbsPath$this->workspaceName]);
  1694.         /*
  1695.          * TODO: https://github.com/jackalope/jackalope-doctrine-dbal/pull/26/files#L0R1057
  1696.          * the other thing i wonder: can't you do the replacement inside sql instead of loading and then storing
  1697.          * the node? this will be extremely slow for a large set of nodes. i think you should use query builder here
  1698.          * rather than raw sql, to make it work on a maximum of platforms.
  1699.          *
  1700.          * can you try to do this please? if we don't figure out how to do it, at least fix the where criteria, and
  1701.          * we can ask the doctrine community how to do the substring operation.
  1702.          * http://stackoverflow.com/questions/8619421/correct-syntax-for-doctrine2s-query-builder-substring-helper-method
  1703.          */
  1704.         $query "UPDATE phpcr_nodes SET ";
  1705.         $updatePathCase "path = CASE ";
  1706.         $updateParentCase "parent = CASE ";
  1707.         $updateLocalNameCase "local_name = CASE ";
  1708.         $updateSortOrderCase "sort_order = CASE ";
  1709.         $updateDepthCase "depth = CASE ";
  1710.         // TODO: Find a better way to do this
  1711.         // Calculate CAST type for CASE statement
  1712.         switch ($this->getConnection()->getDatabasePlatform()->getName()) {
  1713.             case 'pgsql':
  1714.                 $intType 'integer';
  1715.                 break;
  1716.             case 'mysql':
  1717.                 $intType 'unsigned';
  1718.                 break;
  1719.             default:
  1720.                 $intType 'integer';
  1721.         }
  1722.         $i 0;
  1723.         $values $ids = [];
  1724.         $srcAbsPathPattern '/^' preg_quote($srcAbsPath'/') . '/';
  1725.         while ($row $stmt->fetchAssociative()) {
  1726.             $values['id' $i] = $row['id'];
  1727.             $values['path' $i] = preg_replace($srcAbsPathPattern$dstAbsPath$row['path'], 1);
  1728.             $values['parent' $i] = PathHelper::getParentPath($values['path' $i]);
  1729.             $values['depth' $i] = PathHelper::getPathDepth($values['path' $i]);
  1730.             $updatePathCase .= "WHEN id = :id$i THEN :path$i ";
  1731.             $updateParentCase .= "WHEN id = :id$i THEN :parent$i ";
  1732.             $updateDepthCase .= "WHEN id = :id$i THEN CAST(:depth$i AS $intType) ";
  1733.             if ($srcAbsPath === $row['path']) {
  1734.                 [, $localName] = $this->getJcrName($values['path' $i]);
  1735.                 $values['localname' $i] = $localName;
  1736.                 $updateLocalNameCase .= "WHEN id = :id$i THEN :localname$i ";
  1737.                 $updateSortOrderCase .= "WHEN id = :id$i THEN (SELECT * FROM ( SELECT MAX(x.sort_order) + 1 FROM phpcr_nodes x WHERE x.parent = :parent$i) y) ";
  1738.             }
  1739.             $ids[] = $row['id'];
  1740.             $i++;
  1741.         }
  1742.         if (!$i) {
  1743.             return;
  1744.         }
  1745.         $ids implode(','$ids);
  1746.         $updateLocalNameCase .= 'ELSE local_name END, ';
  1747.         $updateSortOrderCase .= 'ELSE sort_order END ';
  1748.         $query .= $updatePathCase 'END, ' $updateParentCase 'END, ' $updateDepthCase 'END, ' $updateLocalNameCase $updateSortOrderCase;
  1749.         $query .= "WHERE id IN ($ids)";
  1750.         try {
  1751.             $this->getConnection()->executeStatement($query$values);
  1752.         } catch (DBALException $e) {
  1753.             throw new RepositoryException(
  1754.                 "Unexpected exception while moving node from $srcAbsPath to $dstAbsPath",
  1755.                 $e->getCode(),
  1756.                 $e
  1757.             );
  1758.         }
  1759.         $this->cleanIdentifierCache($srcAbsPath);
  1760.     }
  1761.     /**
  1762.      * {@inheritDoc}
  1763.      *
  1764.      * @throws RepositoryException
  1765.      */
  1766.     public function reorderChildren(Node $node)
  1767.     {
  1768.         $this->assertLoggedIn();
  1769.         $values['absPath'] = $node->getPath();
  1770.         $sql "UPDATE phpcr_nodes SET sort_order = CASE CONCAT(
  1771.           namespace,
  1772.           (CASE namespace WHEN '' THEN '' ELSE ':' END),
  1773.           local_name
  1774.         )";
  1775.         $i 0;
  1776.         foreach ($node->getNodeNames() as $name) {
  1777.             $values['name' $i] = implode(':'array_filter($this->getJcrName($name)));
  1778.             $values['order' $i] = $i// use our counter to avoid gaps
  1779.             $sql .= " WHEN :name$i THEN :order$i";
  1780.             $i++;
  1781.         }
  1782.         $sql .= ' ELSE sort_order END WHERE parent = :absPath';
  1783.         try {
  1784.             $this->getConnection()->executeStatement($sql$values);
  1785.         } catch (DBALException $e) {
  1786.             throw new RepositoryException('Unexpected exception while reordering nodes'$e->getCode(), $e);
  1787.         }
  1788.         return true;
  1789.     }
  1790.     /**
  1791.      * Return the node processor for processing nodes
  1792.      * according to their node types.
  1793.      *
  1794.      * @return NodeProcessor
  1795.      */
  1796.     private function getNodeProcessor()
  1797.     {
  1798.         if ($this->nodeProcessor) {
  1799.             return $this->nodeProcessor;
  1800.         }
  1801.         $this->nodeProcessor = new NodeProcessor(
  1802.             $this->credentials->getUserID(),
  1803.             $this->getNamespacesObject(),
  1804.             $this->getAutoLastModified()
  1805.         );
  1806.         return $this->nodeProcessor;
  1807.     }
  1808.     /**
  1809.      * {@inheritDoc}
  1810.      */
  1811.     public function storeNodes(array $operations)
  1812.     {
  1813.         $this->assertLoggedIn();
  1814.         $additionalAddOperations = [];
  1815.         foreach ($operations as $operation) {
  1816.             if ($operation->node->isDeleted()) {
  1817.                 $properties $operation->node->getPropertiesForStoreDeletedNode();
  1818.             } else {
  1819.                 $additionalAddOperations array_merge(
  1820.                     $additionalAddOperations,
  1821.                     $this->getNodeProcessor()->process($operation->node)
  1822.                 );
  1823.                 $properties $operation->node->getProperties();
  1824.             }
  1825.             $this->storeNode($operation->srcPath$properties);
  1826.         }
  1827.         if (!empty($additionalAddOperations)) {
  1828.             $this->storeNodes($additionalAddOperations);
  1829.         }
  1830.     }
  1831.     /**
  1832.      * Make sure we have a uuid and a primaryType, then sync data into the database
  1833.      *
  1834.      * @param string $path the path to store the node at
  1835.      * @param Property[] $properties the properties of this node
  1836.      *
  1837.      * @return boolean true on success
  1838.      *
  1839.      * @throws RepositoryException if not logged in
  1840.      */
  1841.     protected function storeNode($path$properties)
  1842.     {
  1843.         $nodeIdentifier $this->getIdentifier($path$properties);
  1844.         $type = isset($properties['jcr:primaryType']) ? $properties['jcr:primaryType']->getValue() : 'nt:unstructured';
  1845.         $this->syncNode($nodeIdentifier$path$typetrue$properties);
  1846.         return true;
  1847.     }
  1848.     /**
  1849.      * Determine a UUID for the node at this path with these properties
  1850.      *
  1851.      * @param string $path
  1852.      * @param Property[] $properties
  1853.      *
  1854.      * @return string a unique id
  1855.      */
  1856.     protected function getIdentifier($path$properties)
  1857.     {
  1858.         if (isset($this->nodeIdentifiers[$path])) {
  1859.             return $this->nodeIdentifiers[$path];
  1860.         }
  1861.         if (isset($properties['jcr:uuid'])) {
  1862.             return $properties['jcr:uuid']->getValue();
  1863.         }
  1864.         // we always generate a uuid, even for non-referenceable nodes that have no automatic uuid
  1865.         return $this->generateUuid();
  1866.     }
  1867.     /**
  1868.      * {@inheritDoc}
  1869.      */
  1870.     public function getNodeTypes($nodeTypes = [])
  1871.     {
  1872.         $standardTypes StandardNodeTypes::getNodeTypeData();
  1873.         $userTypes $this->fetchUserNodeTypes();
  1874.         if ($nodeTypes) {
  1875.             $nodeTypes array_flip($nodeTypes);
  1876.             return array_values(
  1877.                 array_intersect_key($standardTypes$nodeTypes) + array_intersect_key($userTypes$nodeTypes)
  1878.             );
  1879.         }
  1880.         return array_values($standardTypes $userTypes);
  1881.     }
  1882.     /**
  1883.      * Fetch a user-defined node-type definition.
  1884.      *
  1885.      * @return array
  1886.      */
  1887.     protected function fetchUserNodeTypes()
  1888.     {
  1889.         $result = [];
  1890.         $query '
  1891. SELECT
  1892. phpcr_type_nodes.name AS node_name, phpcr_type_nodes.is_abstract AS node_abstract,
  1893. phpcr_type_nodes.is_mixin AS node_mixin, phpcr_type_nodes.queryable AS node_queryable,
  1894. phpcr_type_nodes.orderable_child_nodes AS node_has_orderable_child_nodes,
  1895. phpcr_type_nodes.primary_item AS node_primary_item_name, phpcr_type_nodes.supertypes AS declared_super_type_names,
  1896. phpcr_type_props.name AS property_name, phpcr_type_props.auto_created AS property_auto_created,
  1897. phpcr_type_props.mandatory AS property_mandatory, phpcr_type_props.protected AS property_protected,
  1898. phpcr_type_props.on_parent_version AS property_on_parent_version,
  1899. phpcr_type_props.required_type AS property_required_type, phpcr_type_props.multiple AS property_multiple,
  1900. phpcr_type_props.fulltext_searchable AS property_fulltext_searchable,
  1901. phpcr_type_props.query_orderable AS property_query_orderable, phpcr_type_props.default_value as property_default_value,
  1902. phpcr_type_childs.name AS child_name, phpcr_type_childs.auto_created AS child_auto_created,
  1903. phpcr_type_childs.mandatory AS child_mandatory, phpcr_type_childs.protected AS child_protected,
  1904. phpcr_type_childs.on_parent_version AS child_on_parent_version, phpcr_type_childs.default_type AS child_default_type,
  1905. phpcr_type_childs.primary_types AS child_primary_types
  1906. FROM
  1907. phpcr_type_nodes
  1908. LEFT JOIN
  1909. phpcr_type_props ON phpcr_type_nodes.node_type_id = phpcr_type_props.node_type_id
  1910. LEFT JOIN
  1911. phpcr_type_childs ON phpcr_type_nodes.node_type_id = phpcr_type_childs.node_type_id
  1912. ';
  1913.         $statement $this->getConnection()->prepare($query);
  1914.         $stmtResult $statement->execute();
  1915.         $stmtResult is_bool($stmtResult) ? $statement $stmtResult;
  1916.         while ($row $stmtResult->fetchAssociative()) {
  1917.             $nodeName $row['node_name'];
  1918.             if (!isset($result[$nodeName])) {
  1919.                 $result[$nodeName] = [
  1920.                     'name' => $nodeName,
  1921.                     'isAbstract' => (bool)$row['node_abstract'],
  1922.                     'isMixin' => (bool)$row['node_mixin'],
  1923.                     'isQueryable' => (bool)$row['node_queryable'],
  1924.                     'hasOrderableChildNodes' => (bool)$row['node_has_orderable_child_nodes'],
  1925.                     'primaryItemName' => $row['node_primary_item_name'],
  1926.                     'declaredSuperTypeNames' => array_filter(explode(' '$row['declared_super_type_names'])),
  1927.                     'declaredPropertyDefinitions' => [],
  1928.                     'declaredNodeDefinitions' => [],
  1929.                 ];
  1930.             }
  1931.             if (($propertyName $row['property_name']) !== null) {
  1932.                 $result[$nodeName]['declaredPropertyDefinitions'][] = [
  1933.                     'declaringNodeType' => $nodeName,
  1934.                     'name' => $propertyName,
  1935.                     'isAutoCreated' => (bool)$row['property_auto_created'],
  1936.                     'isMandatory' => (bool)$row['property_mandatory'],
  1937.                     'isProtected' => (bool)$row['property_protected'],
  1938.                     'onParentVersion' => (bool)$row['property_on_parent_version'],
  1939.                     'requiredType' => (int)$row['property_required_type'],
  1940.                     'multiple' => (bool)$row['property_multiple'],
  1941.                     'isFulltextSearchable' => (bool)$row['property_fulltext_searchable'],
  1942.                     'isQueryOrderable' => (bool)$row['property_query_orderable'],
  1943.                     'queryOperators' => [
  1944.                         QOM::JCR_OPERATOR_EQUAL_TO,
  1945.                         QOM::JCR_OPERATOR_NOT_EQUAL_TO,
  1946.                         QOM::JCR_OPERATOR_GREATER_THAN,
  1947.                         QOM::JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO,
  1948.                         QOM::JCR_OPERATOR_LESS_THAN,
  1949.                         QOM::JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO,
  1950.                         QOM::JCR_OPERATOR_LIKE,
  1951.                     ],
  1952.                     'defaultValues' => [$row['property_default_value']]
  1953.                 ];
  1954.             }
  1955.             if (($childName $row['child_name']) !== null) {
  1956.                 $result[$nodeName]['declaredNodeDefinitions'][] = [
  1957.                     'declaringNodeType' => $nodeName,
  1958.                     'name' => $childName,
  1959.                     'isAutoCreated' => (bool)$row['child_auto_created'],
  1960.                     'isMandatory' => (bool)$row['child_mandatory'],
  1961.                     'isProtected' => (bool)$row['child_protected'],
  1962.                     'onParentVersion' => (bool)$row['child_on_parent_version'],
  1963.                     'allowsSameNameSiblings' => false,
  1964.                     'defaultPrimaryTypeName' => $row['child_default_type'],
  1965.                     'requiredPrimaryTypeNames' => array_filter(explode(' '$row['child_primary_types']))
  1966.                 ];
  1967.             }
  1968.         }
  1969.         return $result;
  1970.     }
  1971.     /**
  1972.      * {@inheritDoc}
  1973.      */
  1974.     public function registerNodeTypes($types$allowUpdate)
  1975.     {
  1976.         $builtinTypes StandardNodeTypes::getNodeTypeData();
  1977.         /* @var $type NodeTypeDefinition */
  1978.         foreach ($types as $type) {
  1979.             if (isset($builtinTypes[$type->getName()])) {
  1980.                 throw new RepositoryException(sprintf('%s: can\'t reregister built-in node type.'$type->getName()));
  1981.             }
  1982.             $nodeTypeName $type->getName();
  1983.             $query "SELECT * FROM phpcr_type_nodes WHERE name = ?";
  1984.             $result $this->getConnection()->fetchOne($query, [$nodeTypeName]);
  1985.             $data = [
  1986.                 'name' => $nodeTypeName,
  1987.                 'supertypes' => implode(' '$type->getDeclaredSuperTypeNames()),
  1988.                 'is_abstract' => $type->isAbstract() ? 0,
  1989.                 'is_mixin' => $type->isMixin() ? 0,
  1990.                 'queryable' => $type->isQueryable() ? 0,
  1991.                 'orderable_child_nodes' => $type->hasOrderableChildNodes() ? 0,
  1992.                 'primary_item' => $type->getPrimaryItemName(),
  1993.             ];
  1994.             if ($result) {
  1995.                 if (!$allowUpdate) {
  1996.                     throw new NodeTypeExistsException("Could not register node type with the name '$nodeTypeName'.");
  1997.                 }
  1998.                 $this->getConnection()->update('phpcr_type_nodes'$data, ['node_type_id' => $result]);
  1999.                 $this->getConnection()->delete('phpcr_type_props', ['node_type_id' => $result]);
  2000.                 $this->getConnection()->delete('phpcr_type_childs', ['node_type_id' => $result]);
  2001.                 $nodeTypeId $result;
  2002.             } else {
  2003.                 $this->getConnection()->insert('phpcr_type_nodes'$data);
  2004.                 $nodeTypeId $this->getConnection()->lastInsertId($this->sequenceTypeName);
  2005.             }
  2006.             if ($propDefs $type->getDeclaredPropertyDefinitions()) {
  2007.                 foreach ($propDefs as $propertyDef) {
  2008.                     /* @var $propertyDef PropertyDefinitionInterface */
  2009.                     $this->getConnection()->insert(
  2010.                         'phpcr_type_props',
  2011.                         [
  2012.                             'node_type_id' => $nodeTypeId,
  2013.                             'name' => $propertyDef->getName(),
  2014.                             'protected' => $propertyDef->isProtected() ? 0,
  2015.                             'mandatory' => $propertyDef->isMandatory() ? 0,
  2016.                             'auto_created' => $propertyDef->isAutoCreated() ? 0,
  2017.                             'on_parent_version' => $propertyDef->getOnParentVersion(),
  2018.                             'multiple' => $propertyDef->isMultiple() ? 0,
  2019.                             'fulltext_searchable' => $propertyDef->isFullTextSearchable() ? 0,
  2020.                             'query_orderable' => $propertyDef->isQueryOrderable() ? 0,
  2021.                             'required_type' => $propertyDef->getRequiredType(),
  2022.                             'query_operators' => 0// transform to bitmask
  2023.                             'default_value' => $propertyDef->getDefaultValues() ? current(
  2024.                                 $propertyDef->getDefaultValues()
  2025.                             ) : null,
  2026.                         ]
  2027.                     );
  2028.                 }
  2029.             }
  2030.             if ($childDefs $type->getDeclaredChildNodeDefinitions()) {
  2031.                 foreach ($childDefs as $childDef) {
  2032.                     /* @var $childDef NodeDefinitionInterface */
  2033.                     $this->getConnection()->insert(
  2034.                         'phpcr_type_childs',
  2035.                         [
  2036.                             'node_type_id' => $nodeTypeId,
  2037.                             'name' => $childDef->getName(),
  2038.                             'protected' => $childDef->isProtected() ? 0,
  2039.                             'mandatory' => $childDef->isMandatory() ? 0,
  2040.                             'auto_created' => $childDef->isAutoCreated() ? 0,
  2041.                             'on_parent_version' => $childDef->getOnParentVersion(),
  2042.                             'primary_types' => implode(' '$childDef->getRequiredPrimaryTypeNames() ?: []),
  2043.                             'default_type' => $childDef->getDefaultPrimaryTypeName(),
  2044.                         ]
  2045.                     );
  2046.                 }
  2047.             }
  2048.         }
  2049.     }
  2050.     /**
  2051.      * {@inheritDoc}
  2052.      */
  2053.     public function setNodeTypeManager($nodeTypeManager)
  2054.     {
  2055.         $this->nodeTypeManager $nodeTypeManager;
  2056.     }
  2057.     /**
  2058.      * {@inheritDoc}
  2059.      */
  2060.     public function cloneFrom($srcWorkspace$srcAbsPath$destAbsPath$removeExisting)
  2061.     {
  2062.         throw new NotImplementedException('Cloning nodes is not implemented yet');
  2063.     }
  2064.     /**
  2065.      * {@inheritDoc}
  2066.      */
  2067.     public function updateNode(Node $node$srcWorkspace)
  2068.     {
  2069.         throw new NotImplementedException('Updating nodes is not implemented yet');
  2070.     }
  2071.     /**
  2072.      * {@inheritDoc}
  2073.      * @throws RepositoryException when no binary data found
  2074.      */
  2075.     public function getBinaryStream($path)
  2076.     {
  2077.         $this->assertLoggedIn();
  2078.         $nodePath PathHelper::getParentPath($path);
  2079.         $nodeId $this->getSystemIdForNode($nodePath);
  2080.         $propertyName PathHelper::getNodeName($path);
  2081.         $data $this->getConnection()->fetchAllAssociative(
  2082.             'SELECT data, idx FROM phpcr_binarydata WHERE node_id = ? AND property_name = ? AND workspace_name = ?',
  2083.             [$nodeId$propertyName$this->workspaceName]
  2084.         );
  2085.         if (count($data) === 0) {
  2086.             throw new RepositoryException('No binary data found in stream');
  2087.         }
  2088.         $streams = [];
  2089.         foreach ($data as $row) {
  2090.             if (is_resource($row['data'])) {
  2091.                 $stream $row['data'];
  2092.             } else {
  2093.                 $stream fopen('php://memory''rwb+');
  2094.                 fwrite($stream$row['data']);
  2095.                 rewind($stream);
  2096.             }
  2097.             $streams[] = $stream;
  2098.         }
  2099.         if (count($data) === 1) {
  2100.             // we don't know if this is a multivalue property or not.
  2101.             // TODO we should have something more efficient to know this. a flag in the database?
  2102.             // TODO use self::getProperty()->isMultiple() once implemented
  2103.             $node $this->getNode($nodePath);
  2104.             if (!is_array($node->{':' $propertyName})) {
  2105.                 return reset($streams);
  2106.             }
  2107.         }
  2108.         return $streams;
  2109.     }
  2110.     /**
  2111.      * {@inheritDoc}
  2112.      */
  2113.     public function getProperty($path)
  2114.     {
  2115.         throw new NotImplementedException('Getting properties by path is not implemented yet');
  2116.     }
  2117.     /**
  2118.      * {@inheritDoc}
  2119.      *
  2120.      * @throws InvalidQueryException
  2121.      */
  2122.     public function query(Query $query)
  2123.     {
  2124.         $this->assertLoggedIn();
  2125.         if (!$query instanceof QueryObjectModelInterface) {
  2126.             $parser = new Sql2ToQomQueryConverter($this->factory->get(QueryObjectModelFactory::class));
  2127.             try {
  2128.                 $qom $parser->parse($query->getStatement());
  2129.                 $qom->setLimit($query->getLimit());
  2130.                 $qom->setOffset($query->getOffset());
  2131.             } catch (Exception $e) {
  2132.                 throw new InvalidQueryException('Invalid query: ' $query->getStatement(), 0$e);
  2133.             }
  2134.         } else {
  2135.             $qom $query;
  2136.         }
  2137.         $qomWalker = new QOMWalker($this->nodeTypeManager$this->getConnection(), $this->getNamespaces());
  2138.         [$selectors$selectorAliases$sql] = $qomWalker->walkQOMQuery($qom);
  2139.         $primarySource reset($selectors);
  2140.         $primaryType $primarySource->getSelectorName() ?: $primarySource->getNodeTypeName();
  2141.         $statement $this->getConnection()->executeQuery($sql, [$this->workspaceName]);
  2142.         $results $properties $standardColumns = [];
  2143.         while ($row $statement->fetchAssociative()) {
  2144.             $result = [];
  2145.             /** @var SelectorInterface $selector */
  2146.             foreach ($selectors as $selector) {
  2147.                 $selectorName $selector->getSelectorName() ?: $selector->getNodeTypeName();
  2148.                 $columnPrefix = isset($selectorAliases[$selectorName]) ? $selectorAliases[$selectorName] . '_' $selectorAliases[''] . '_';
  2149.                 if ($primaryType === $selector->getNodeTypeName()) {
  2150.                     $result[] = [
  2151.                         'dcr:name' => 'jcr:path',
  2152.                         'dcr:value' => $row[$columnPrefix 'path'],
  2153.                         'dcr:selectorName' => $selectorName
  2154.                     ];
  2155.                 }
  2156.                 $result[] = [
  2157.                     'dcr:name' => 'jcr:path',
  2158.                     'dcr:value' => $row[$columnPrefix 'path'],
  2159.                     'dcr:selectorName' => $selectorName
  2160.                 ];
  2161.                 $result[] = ['dcr:name' => 'jcr:score''dcr:value' => 0'dcr:selectorName' => $selectorName];
  2162.                 if (=== count($qom->getColumns())) {
  2163.                     $selectorPrefix null !== $selector->getSelectorName() ? $selectorName '.' '';
  2164.                     $result[] = [
  2165.                         'dcr:name' => $selectorPrefix 'jcr:primaryType',
  2166.                         'dcr:value' => $primaryType,
  2167.                         'dcr:selectorName' => $selectorName
  2168.                     ];
  2169.                 }
  2170.                 if (isset($row[$columnPrefix 'props'])) {
  2171.                     $propertyNames = [];
  2172.                     $columns $qom->getColumns();
  2173.                     // Always populate jcr:created and jcr:createdBy if a wildcard selector is used.
  2174.                     // This emulates the behavior of Jackrabbit
  2175.                     if (=== count($columns)) {
  2176.                         $propertyNames = ['jcr:created''jcr:createdBy'];
  2177.                     }
  2178.                     foreach ($columns as $column) {
  2179.                         if (!$column->getSelectorName() || $column->getSelectorName() == $selectorName) {
  2180.                             $propertyNames[] = $column->getPropertyName();
  2181.                         }
  2182.                     }
  2183.                     $properties[$selectorName] = (array)$this->xmlToColumns(
  2184.                         $row[$columnPrefix 'props'],
  2185.                         $propertyNames
  2186.                     );
  2187.                 } else {
  2188.                     $properties[$selectorName] = [];
  2189.                 }
  2190.                 // TODO: add other default columns that Jackrabbit provides to provide a more consistent behavior
  2191.                 if (isset($properties[$selectorName]['jcr:createdBy'])) {
  2192.                     $standardColumns[$selectorName]['jcr:createdBy'] = $properties[$selectorName]['jcr:createdBy'];
  2193.                 }
  2194.                 if (isset($properties[$selectorName]['jcr:created'])) {
  2195.                     $standardColumns[$selectorName]['jcr:created'] = $properties[$selectorName]['jcr:created'];
  2196.                 }
  2197.             }
  2198.             $reservedNames = ['jcr:path''jcr:score'];
  2199.             foreach ($qom->getColumns() as $column) {
  2200.                 $selectorName $column->getSelectorName();
  2201.                 $columnName $column->getPropertyName();
  2202.                 $columnPrefix = isset($selectorAliases[$selectorName]) ? $selectorAliases[$selectorName] . '_' $selectorAliases[''] . '_';
  2203.                 if (in_array($column->getColumnName(), $reservedNames)) {
  2204.                     throw new InvalidQueryException(
  2205.                         sprintf(
  2206.                             'Cannot reserved name "%s". Reserved names are "%s"',
  2207.                             $column->getColumnName(),
  2208.                             implode('", "'$reservedNames)
  2209.                         )
  2210.                     );
  2211.                 }
  2212.                 $dcrValue 'jcr:uuid' === $columnName $row[$columnPrefix 'identifier'] : (isset($properties[$selectorName][$columnName]) ? $properties[$selectorName][$columnName] : '');
  2213.                 if (isset($standardColumns[$selectorName][$columnName])) {
  2214.                     unset($standardColumns[$selectorName][$columnName]);
  2215.                 }
  2216.                 $result[] = [
  2217.                     'dcr:name' => $column->getColumnName(
  2218.                     ) === $columnName && isset($properties[$selectorName][$columnName]) ? $selectorName '.' $columnName $column->getColumnName(
  2219.                     ),
  2220.                     'dcr:value' => $dcrValue,
  2221.                     'dcr:selectorName' => $selectorName ?: $primaryType,
  2222.                 ];
  2223.             }
  2224.             foreach ($standardColumns as $selectorName => $columns) {
  2225.                 foreach ($columns as $columnName => $value) {
  2226.                     $result[] = [
  2227.                         'dcr:name' => $primaryType '.' $columnName,
  2228.                         'dcr:value' => $value,
  2229.                         'dcr:selectorName' => $selectorName,
  2230.                     ];
  2231.                 }
  2232.             }
  2233.             $results[] = $result;
  2234.         }
  2235.         return $results;
  2236.     }
  2237.     /**
  2238.      * {@inheritDoc}
  2239.      */
  2240.     public function getSupportedQueryLanguages()
  2241.     {
  2242.         return [
  2243.             QueryInterface::JCR_SQL2,
  2244.             QueryInterface::JCR_JQOM,
  2245.             QueryInterface::SQL,
  2246.         ];
  2247.     }
  2248.     /**
  2249.      * We need to create an in memory backup when we are inside a transaction
  2250.      * so that we can efficiently restore the original state in the namespaces
  2251.      * property in case of a rollback.
  2252.      *
  2253.      * This method also ensures that namespaces are loaded to begin with.
  2254.      */
  2255.     private function ensureNamespacesBackup()
  2256.     {
  2257.         if (!$this->namespaces instanceof \ArrayObject) {
  2258.             $this->getNamespacesObject();
  2259.         }
  2260.         if (!$this->inTransaction) {
  2261.             return;
  2262.         }
  2263.         if (null === $this->originalNamespaces) {
  2264.             $this->originalNamespaces $this->namespaces->getArrayCopy();
  2265.         }
  2266.     }
  2267.     /**
  2268.      * {@inheritDoc}
  2269.      *
  2270.      * @throws NamespaceException
  2271.      */
  2272.     public function registerNamespace($prefix$uri)
  2273.     {
  2274.         if (isset($this->namespaces[$prefix])) {
  2275.             if ($this->namespaces[$prefix] === $uri) {
  2276.                 return;
  2277.             }
  2278.             if (isset($this->coreNamespaces[$prefix])) {
  2279.                 throw new NamespaceException(
  2280.                     "Cannot overwrite JCR core namespace prefix '$prefix' to a new uri '$uri'."
  2281.                 );
  2282.             }
  2283.         }
  2284.         $this->ensureNamespacesBackup();
  2285.         $this->getConnection()->delete('phpcr_namespaces', ['prefix' => $prefix]);
  2286.         $this->getConnection()->delete('phpcr_namespaces', ['uri' => $uri]);
  2287.         $this->getConnection()->insert(
  2288.             'phpcr_namespaces',
  2289.             [
  2290.                 'prefix' => $prefix,
  2291.                 'uri' => $uri,
  2292.             ]
  2293.         );
  2294.         if (!empty($this->namespaces)) {
  2295.             $this->namespaces[$prefix] = $uri;
  2296.         }
  2297.     }
  2298.     /**
  2299.      * {@inheritDoc}
  2300.      */
  2301.     public function unregisterNamespace($prefix)
  2302.     {
  2303.         if (isset($this->coreNamespaces[$prefix])) {
  2304.             throw new NamespaceException("Cannot unregister JCR core namespace prefix '$prefix'.");
  2305.         }
  2306.         $this->ensureNamespacesBackup();
  2307.         $this->getConnection()->delete('phpcr_namespaces', ['prefix' => $prefix]);
  2308.         if (!empty($this->namespaces)) {
  2309.             unset($this->namespaces[$prefix]);
  2310.         }
  2311.     }
  2312.     /**
  2313.      * {@inheritDoc}
  2314.      */
  2315.     public function getReferences($path$name null)
  2316.     {
  2317.         return $this->getNodeReferences($path$namefalse);
  2318.     }
  2319.     /**
  2320.      * {@inheritDoc}
  2321.      */
  2322.     public function getWeakReferences($path$name null)
  2323.     {
  2324.         return $this->getNodeReferences($path$nametrue);
  2325.     }
  2326.     /**
  2327.      * @param string $path the path for which we need the references
  2328.      * @param string $name the name of the referencing properties or null for all
  2329.      * @param boolean $weakReference whether to get weak or strong references
  2330.      *
  2331.      * @return array list of paths to nodes that reference $path
  2332.      */
  2333.     private function getNodeReferences($path$name null$weakReference false)
  2334.     {
  2335.         $targetId $this->getSystemIdForNode($path);
  2336.         if (false === $targetId) {
  2337.             return [];
  2338.         }
  2339.         $params = [$targetId];
  2340.         $table $weakReference $this->referenceTables[PropertyType::WEAKREFERENCE] : $this->referenceTables[PropertyType::REFERENCE];
  2341.         $query "SELECT CONCAT(n.path, '/', r.source_property_name) FROM phpcr_nodes n
  2342.                INNER JOIN $table r ON n.id = r.source_id
  2343.                WHERE r.target_id = ?";
  2344.         if (null !== $name) {
  2345.             $query .= " AND source_property_name = ?";
  2346.             $params[] = $name;
  2347.         }
  2348.         $stmt $this->getConnection()->executeQuery($query$params);
  2349.         return array_column($stmt->fetchAllNumeric(), 0);
  2350.     }
  2351.     /**
  2352.      * {@inheritDoc}
  2353.      */
  2354.     public function beginTransaction()
  2355.     {
  2356.         if ($this->inTransaction) {
  2357.             throw new RepositoryException('Begin transaction failed: transaction already open');
  2358.         }
  2359.         $this->assertLoggedIn();
  2360.         try {
  2361.             $this->getConnection()->beginTransaction();
  2362.             $this->inTransaction true;
  2363.         } catch (Exception $e) {
  2364.             throw new RepositoryException('Begin transaction failed: ' $e->getMessage());
  2365.         }
  2366.     }
  2367.     /**
  2368.      * {@inheritDoc}
  2369.      *
  2370.      * @throws RepositoryException
  2371.      */
  2372.     public function commitTransaction()
  2373.     {
  2374.         if (!$this->inTransaction) {
  2375.             throw new RepositoryException('Commit transaction failed: no transaction open');
  2376.         }
  2377.         $this->assertLoggedIn();
  2378.         try {
  2379.             $this->inTransaction false;
  2380.             $this->getConnection()->commit();
  2381.             if ($this->originalNamespaces) {
  2382.                 // now that the transaction is committed, reset the cache of the stored namespaces.
  2383.                 $this->originalNamespaces null;
  2384.             }
  2385.         } catch (Exception $e) {
  2386.             throw new RepositoryException('Commit transaction failed: ' $e->getMessage());
  2387.         }
  2388.     }
  2389.     /**
  2390.      * {@inheritDoc}
  2391.      *
  2392.      * @throws RepositoryException
  2393.      */
  2394.     public function rollbackTransaction()
  2395.     {
  2396.         if (!$this->inTransaction) {
  2397.             throw new RepositoryException('Rollback transaction failed: no transaction open');
  2398.         }
  2399.         $this->assertLoggedIn();
  2400.         try {
  2401.             $this->inTransaction false;
  2402.             $this->getConnection()->rollBack();
  2403.             if ($this->originalNamespaces) {
  2404.                 // reset namespaces
  2405.                 $this->setNamespaces($this->originalNamespaces);
  2406.                 $this->originalNamespaces null;
  2407.             }
  2408.         } catch (Exception $e) {
  2409.             throw new RepositoryException('Rollback transaction failed: ' $e->getMessage(), 0$e);
  2410.         }
  2411.     }
  2412.     /**
  2413.      * Sets the default transaction timeout
  2414.      *
  2415.      * @param int $seconds The value of the timeout in seconds
  2416.      */
  2417.     public function setTransactionTimeout($seconds)
  2418.     {
  2419.         $this->assertLoggedIn();
  2420.         throw new NotImplementedException("Setting a transaction timeout is not yet implemented");
  2421.     }
  2422.     /**
  2423.      * {@inheritDoc}
  2424.      */
  2425.     public function prepareSave()
  2426.     {
  2427.         $this->getConnection()->beginTransaction();
  2428.     }
  2429.     /**
  2430.      * {@inheritDoc}
  2431.      */
  2432.     public function finishSave()
  2433.     {
  2434.         $this->syncReferences($this->referencesToUpdate);
  2435.         $this->referencesToUpdate $this->referencesToDelete = [];
  2436.         $this->getConnection()->commit();
  2437.     }
  2438.     /**
  2439.      * {@inheritDoc}
  2440.      */
  2441.     public function rollbackSave()
  2442.     {
  2443.         $this->referencesToUpdate $this->referencesToDelete = [];
  2444.         $this->getConnection()->rollBack();
  2445.     }
  2446.     /**
  2447.      *
  2448.      * @param Node $node the node to update
  2449.      */
  2450.     public function updateProperties(Node $node)
  2451.     {
  2452.         $this->assertLoggedIn();
  2453.         // we can ignore the operations returned, there will be no additions because of property updates
  2454.         $this->getNodeProcessor()->process($node);
  2455.         $this->syncNode(
  2456.             $node->getIdentifier(),
  2457.             $node->getPath(),
  2458.             $node->getPrimaryNodeType(),
  2459.             false,
  2460.             $node->getProperties()
  2461.         );
  2462.         return true;
  2463.     }
  2464.     /**
  2465.      * Initialize the dbal connection lazily
  2466.      */
  2467.     private function initConnection()
  2468.     {
  2469.         if (true === $this->conn->isConnected() && true === $this->connectionInitialized) {
  2470.             return;
  2471.         }
  2472.         $platform $this->conn->getDatabasePlatform();
  2473.         if ($platform instanceof PostgreSQL94Platform || $platform instanceof PostgreSqlPlatform) {
  2474.             $this->sequenceNodeName 'phpcr_nodes_id_seq';
  2475.             $this->sequenceTypeName 'phpcr_type_nodes_node_type_id_seq';
  2476.         }
  2477.         // @TODO: move to "SqlitePlatform" and rename to "registerExtraFunctions"?
  2478.         if ($this->conn->getDatabasePlatform() instanceof SqlitePlatform) {
  2479.             if (method_exists($this->conn'getNativeConnection')) {
  2480.                 $connection $this->conn->getNativeConnection();
  2481.             } else {
  2482.                 $connection $this->conn->getWrappedConnection();
  2483.                 if ($connection instanceof PDOConnection && !$connection instanceof PDO) {
  2484.                     $connection $connection->getWrappedConnection();
  2485.                 }
  2486.             }
  2487.             $this->registerSqliteFunctions($connection);
  2488.         }
  2489.         $this->connectionInitialized true;
  2490.     }
  2491. }