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

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