From e255b4b7a2041efb01f3f161409815dca2d7a5ec Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Wed, 6 Jul 2022 00:42:49 +0200 Subject: [PATCH 1/6] Introduce Doctrine ORM TODOs: - [x] EntityManager wrapper - [ ] EntityRepository wrapper - [ ] Query wrapper (for the DQL) - [x] Command integration - [ ] Psr6 Cache wrapper around the ICache - [ ] Decide if we want a wrapper for the Mapping DTO class (lot of work not much beneficts) Signed-off-by: Carl Schwan --- core/register_command.php | 9 ++ lib/private/DB/ConnectionFactory.php | 2 +- lib/private/DB/ORM/EntityManagerAdapter.php | 88 ++++++++++++ .../DB/ORM/EntityRepositoryAdapter.php | 15 ++ lib/private/DB/ORM/Psr6CacheAdapter.php | 116 +++++++++++++++ lib/private/DB/ORM/QueryAdapter.php | 14 ++ lib/private/DB/ORM/TablePrefix.php | 34 +++++ lib/private/Server.php | 9 ++ lib/public/DB/ORM/IEntityManager.php | 133 ++++++++++++++++++ lib/public/DB/ORM/IEntityRepository.php | 8 ++ lib/public/DB/ORM/IQuery.php | 8 ++ lib/public/DB/ORM/LockMode.php | 8 ++ lib/public/DB/ORM/OptimisticLockException.php | 6 + .../DB/ORM/PessimisticLockException.php | 7 + 14 files changed, 456 insertions(+), 1 deletion(-) create mode 100644 lib/private/DB/ORM/EntityManagerAdapter.php create mode 100644 lib/private/DB/ORM/EntityRepositoryAdapter.php create mode 100644 lib/private/DB/ORM/Psr6CacheAdapter.php create mode 100644 lib/private/DB/ORM/QueryAdapter.php create mode 100644 lib/private/DB/ORM/TablePrefix.php create mode 100644 lib/public/DB/ORM/IEntityManager.php create mode 100644 lib/public/DB/ORM/IEntityRepository.php create mode 100644 lib/public/DB/ORM/IQuery.php create mode 100644 lib/public/DB/ORM/LockMode.php create mode 100644 lib/public/DB/ORM/OptimisticLockException.php create mode 100644 lib/public/DB/ORM/PessimisticLockException.php diff --git a/core/register_command.php b/core/register_command.php index 98a653aed7e41..d7a365d1426cb 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -115,6 +115,15 @@ $application->add(new OC\Core\Command\Db\Migrations\MigrateCommand(\OC::$server->get(\OC\DB\Connection::class))); $application->add(new OC\Core\Command\Db\Migrations\GenerateCommand(\OC::$server->get(\OC\DB\Connection::class), \OC::$server->getAppManager())); $application->add(new OC\Core\Command\Db\Migrations\ExecuteCommand(\OC::$server->get(\OC\DB\Connection::class), \OC::$server->getConfig())); + Doctrine\ORM\Tools\Console\ConsoleRunner::addCommands($application, new class implements \Doctrine\ORM\Tools\Console\EntityManagerProvider { + public function getDefaultManager(): \Doctrine\ORM\EntityManagerInterface { + return \OCP\Server::get(\OCP\DB\ORM\IEntityManager::class)->get(); + } + + public function getManager(string $name): \Doctrine\ORM\EntityManagerInterface { + return \OCP\Server::get(\OCP\DB\ORM\IEntityManager::class)->get(); + } + }); } $application->add(new OC\Core\Command\Encryption\Disable(\OC::$server->getConfig())); diff --git a/lib/private/DB/ConnectionFactory.php b/lib/private/DB/ConnectionFactory.php index 95f3185bcdb17..c427e4fdb8bd4 100644 --- a/lib/private/DB/ConnectionFactory.php +++ b/lib/private/DB/ConnectionFactory.php @@ -29,7 +29,7 @@ namespace OC\DB; use Doctrine\Common\EventManager; -use Doctrine\DBAL\Configuration; +use Doctrine\ORM\Configuration; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Event\Listeners\OracleSessionInit; use Doctrine\DBAL\Event\Listeners\SQLSessionInit; diff --git a/lib/private/DB/ORM/EntityManagerAdapter.php b/lib/private/DB/ORM/EntityManagerAdapter.php new file mode 100644 index 0000000000000..3e702d788b554 --- /dev/null +++ b/lib/private/DB/ORM/EntityManagerAdapter.php @@ -0,0 +1,88 @@ + \OC_App::getAppPath($appId) . '/lib/Entity/', \OC_App::getEnabledApps()), fn ($path) => is_dir($path)); + $isDevMode = true; + $proxyDir = null; + $cache = null; + + + $evm = $connection->getInner()->getEventManager(); + $tablePrefix = new TablePrefix('oc_'); + + $evm->addEventListener(\Doctrine\ORM\Events::loadClassMetadata, $tablePrefix); + // TODO actually use our cache with a psr6 cache wrapper or at least our cache config + $config = ORMSetup::createAnnotationMetadataConfiguration($paths, $isDevMode, $proxyDir, $cache); + + $this->em = EntityManager::create($connection->getInner(), $config, $evm); + $this->connection = $connection; + } + + public function createQuery($dql = ''): IQuery + { + return new QueryAdapter($this->em->createQuery($dql)); + } + + public function flush(): void { + $this->em->flush(); + } + + public function find(string $className, $id, ?int $lockMode = null, ?int $lockVersion = null): ?object { + return $this->em->find($className, $id, $lockMode, $lockVersion); + } + + public function clear(): void { + $this->em->clear(); + } + + public function persist(object $entity): void { + $this->em->persist($entity); + } + + public function remove(object $entity): void { + $this->em->remove($entity); + } + + public function lock(object $entity, int $lockMode, $lockVersion = null): void { + $this->em->lock($entity, $lockMode, $lockVersion); + } + + public function getRepository($className): IEntityRepository { + /** @var EntityRepository $internalRepo */ + $internalRepo = $this->em->getRepository($className); + return new EntityRepositoryAdapter($internalRepo); + } + + public function contains(object $entity): bool { + return $this->em->contains($entity); + } + + public function getConnection(): IDBConnection { + return $this->connection; + } + + /** + * Only for internal use + */ + public function get(): EntityManager { + return $this->em; + } + +} diff --git a/lib/private/DB/ORM/EntityRepositoryAdapter.php b/lib/private/DB/ORM/EntityRepositoryAdapter.php new file mode 100644 index 0000000000000..50b2c5b19f12a --- /dev/null +++ b/lib/private/DB/ORM/EntityRepositoryAdapter.php @@ -0,0 +1,15 @@ +entityRepository = $entityRepository; + } +} diff --git a/lib/private/DB/ORM/Psr6CacheAdapter.php b/lib/private/DB/ORM/Psr6CacheAdapter.php new file mode 100644 index 0000000000000..9d324e33b2ed2 --- /dev/null +++ b/lib/private/DB/ORM/Psr6CacheAdapter.php @@ -0,0 +1,116 @@ +cache = $cache; + $this->key = $key; + } + + private function fetch(): void { + if (!$this->fetched) { + $this->value = $this->cache->get($this->key); + $this->fetched = true; + } + } + + public function getKey() { + return $this->key; + } + + public function get() { + $this->fetch(); + return $this->value; + } + + public function isHit() { + $this->fetch(); + return $this->value !== null; + } + + public function set($value) { + $this->value = $value; + } + + public function expiresAt($expiration) { + $this->_expireAt = $expiration; + } + + public function expiresAfter($time) { + $this->_expireAfter = $time; + } + + public function getExpireAt(): ?\DateTime + { + return $this->_expireAt; + } + + public function getExpireAfter(): int + { + return $this->_expireAfter; + } +} + +class Psr6CacheAdapter implements CacheItemPoolInterface { + private ICache $cache; + + public function __construct(ICache $cache) { + $this->cache = $cache; + } + + public function getItem($key) { + return new CacheItemAdapter($this->cache, $key); + } + + public function getItems(array $keys = array()) { + for (int ) + // TODO: Implement getItems() method. + } + + public function hasItem($key) + { + // TODO: Implement hasItem() method. + } + + public function clear() + { + // TODO: Implement clear() method. + } + + public function deleteItem($key) + { + // TODO: Implement deleteItem() method. + } + + public function deleteItems(array $keys) + { + // TODO: Implement deleteItems() method. + } + + public function save(CacheItemInterface $item) + { + // TODO: Implement save() method. + } + + public function saveDeferred(CacheItemInterface $item) + { + // TODO: Implement saveDeferred() method. + } + + public function commit() + { + // TODO: Implement commit() method. + } +} diff --git a/lib/private/DB/ORM/QueryAdapter.php b/lib/private/DB/ORM/QueryAdapter.php new file mode 100644 index 0000000000000..6de031e00240a --- /dev/null +++ b/lib/private/DB/ORM/QueryAdapter.php @@ -0,0 +1,14 @@ +query = $query; + } +} diff --git a/lib/private/DB/ORM/TablePrefix.php b/lib/private/DB/ORM/TablePrefix.php new file mode 100644 index 0000000000000..168568bee03ae --- /dev/null +++ b/lib/private/DB/ORM/TablePrefix.php @@ -0,0 +1,34 @@ +prefix = (string)$prefix; + } + + public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs) + { + $classMetadata = $eventArgs->getClassMetadata(); + + if (!$classMetadata->isInheritanceTypeSingleTable() || $classMetadata->getName() === $classMetadata->rootEntityName) { + $classMetadata->setPrimaryTable([ + 'name' => $this->prefix . $classMetadata->getTableName() + ]); + } + + foreach ($classMetadata->getAssociationMappings() as $fieldName => $mapping) { + if ($mapping['type'] == \Doctrine\ORM\Mapping\ClassMetadataInfo::MANY_TO_MANY && $mapping['isOwningSide']) { + $mappedTableName = $mapping['joinTable']['name']; + $classMetadata->associationMappings[$fieldName]['joinTable']['name'] = $this->prefix . $mappedTableName; + } + } + } + +} diff --git a/lib/private/Server.php b/lib/private/Server.php index 7223c3b8ae374..d996e99838ba3 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -80,6 +80,7 @@ use OC\Dashboard\DashboardManager; use OC\DB\Connection; use OC\DB\ConnectionAdapter; +use OC\DB\ORM\EntityManagerAdapter; use OC\Diagnostics\EventLogger; use OC\Diagnostics\QueryLogger; use OC\EventDispatcher\SymfonyAdapter; @@ -167,6 +168,7 @@ use OCP\Contacts\ContactsMenu\IActionFactory; use OCP\Contacts\ContactsMenu\IContactsStore; use OCP\Dashboard\IDashboardManager; +use OCP\DB\ORM\IEntityManager; use OCP\Defaults; use OCP\Diagnostics\IEventLogger; use OCP\Diagnostics\IQueryLogger; @@ -878,6 +880,13 @@ public function __construct($webRoot, \OC\Config $config) { $connection = $factory->getConnection($type, $connectionParams); return $connection; }); + + $this->registerService(IEntityManager::class, function (ContainerInterface $c): IEntityManager { + /** @var ConnectionAdapter $connection */ + $connection = $c->get(IDBConnection::class); + return new EntityManagerAdapter($connection); + }); + /** @deprecated 19.0.0 */ $this->registerDeprecatedAlias('DatabaseConnection', IDBConnection::class); diff --git a/lib/public/DB/ORM/IEntityManager.php b/lib/public/DB/ORM/IEntityManager.php new file mode 100644 index 0000000000000..355c743b6043e --- /dev/null +++ b/lib/public/DB/ORM/IEntityManager.php @@ -0,0 +1,133 @@ + $className + * @psalm-param LockMode::*|null $lockMode + * + * @return object|null The entity instance or NULL if the entity can not be found. + * @psalm-return ?T + * + * @throws OptimisticLockException + * @throws \OCP\DB\Exception + * + * @template T + * @since 25.0.0 + */ + public function find(string $className, $id, ?int $lockMode = null, ?int $lockVersion = null): ?object; + + /** + * Clears the EntityManager. All entities that are currently managed + * by this EntityManager become detached. + * + * @throws \OCP\DB\Exception If a $entityName is given, but that entity is not + * found in the mappings. + * @since 25.0.0 + */ + public function clear(): void; + + /** + * Tells the EntityManager to make an instance managed and persistent. + * + * The entity will be entered into the database at or before transaction + * commit or as a result of the flush operation. + * + * NOTE: The persist operation always considers entities that are not yet known to + * this EntityManager as NEW. Do not pass detached entities to the persist operation. + * + * @param object $entity The instance to make managed and persistent. + * + * @throws \OCP\DB\Exception + * @since 25.0.0 + */ + public function persist(object $entity): void; + + /** + * Removes an entity instance. + * + * A removed entity will be removed from the database at or before transaction commit + * or as a result of the flush operation. + * + * @param object $entity The entity instance to remove. + * + @throws \OCP\DB\Exception + * @since 25.0.0 + */ + public function remove(object $entity): void; + + /** + * Acquire a lock on the given entity. + * + * @param int|DateTimeInterface|null $lockVersion + * @psalm-param LockMode::* $lockMode + * + * + * @throws OptimisticLockException + * @throws PessimisticLockException + * @since 25.0.0 + */ + public function lock(object $entity, int $lockMode, $lockVersion = null): void; + + /** + * {@inheritdoc} + * + * @psalm-param class-string $className + * + * @psalm-return IEntityRepository + * + * @template T of object + * @since 25.0.0 + */ + public function getRepository($className): IEntityRepository; + + /** + * Determines whether an entity instance is managed in this EntityManager. + * + * @return bool TRUE if this EntityManager currently manages the given entity, FALSE otherwise. + * @since 25.0.0 + */ + public function contains(object $entity): bool; + + /** + * @return IDBConnection + * @since 25.0.0 + */ + public function getConnection(): IDBConnection; +} diff --git a/lib/public/DB/ORM/IEntityRepository.php b/lib/public/DB/ORM/IEntityRepository.php new file mode 100644 index 0000000000000..50c7d2c1fa95a --- /dev/null +++ b/lib/public/DB/ORM/IEntityRepository.php @@ -0,0 +1,8 @@ + Date: Wed, 6 Jul 2022 14:30:52 +0200 Subject: [PATCH 2/6] fixup! Introduce Doctrine ORM --- .../DB/ORM/EntityRepositoryAdapter.php | 20 ++++++ lib/public/DB/ORM/IEntityRepository.php | 61 +++++++++++++++++++ lib/public/DB/ORM/IParameter.php | 37 +++++++++++ lib/public/DB/ORM/IQuery.php | 53 +++++++++++++++- 4 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 lib/public/DB/ORM/IParameter.php diff --git a/lib/private/DB/ORM/EntityRepositoryAdapter.php b/lib/private/DB/ORM/EntityRepositoryAdapter.php index 50b2c5b19f12a..b86baed4139f6 100644 --- a/lib/private/DB/ORM/EntityRepositoryAdapter.php +++ b/lib/private/DB/ORM/EntityRepositoryAdapter.php @@ -12,4 +12,24 @@ class EntityRepositoryAdapter implements IEntityRepository public function __construct(EntityRepository $entityRepository) { $this->entityRepository = $entityRepository; } + + public function find($id) { + return $this->entityRepository->find($id); + } + + public function findAll() { + return $this->entityRepository->findAll(); + } + + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null) { + return $this->entityRepository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria) { + return $this->entityRepository->findOneBy($criteria); + } + + public function getClassName() { + return $this->entityRepository->getClassName(); + } } diff --git a/lib/public/DB/ORM/IEntityRepository.php b/lib/public/DB/ORM/IEntityRepository.php index 50c7d2c1fa95a..3ec5f72b1f728 100644 --- a/lib/public/DB/ORM/IEntityRepository.php +++ b/lib/public/DB/ORM/IEntityRepository.php @@ -2,7 +2,68 @@ namespace OCP\DB\ORM; +/** + * Contract for a Doctrine persistence layer ObjectRepository class to implement. + * + * @template-covariant T of object + */ interface IEntityRepository { + /** + * Finds an object by its primary key / identifier. + * + * @param mixed $id The identifier. + * + * @return object|null The object. + * @psalm-return T|null + */ + public function find($id); + /** + * Finds all objects in the repository. + * + * @return array The objects. + * @psalm-return T[] + */ + public function findAll(); + + /** + * Finds objects by a set of criteria. + * + * Optionally sorting and limiting details can be passed. An implementation may throw + * an UnexpectedValueException if certain values of the sorting or limiting details are + * not supported. + * + * @param array $criteria + * @param array|null $orderBy + * @psalm-param array|null $orderBy + * + * @return array The objects. + * @psalm-return T[] + * + * @throws \RuntimeException + */ + public function findBy( + array $criteria, + ?array $orderBy = null, + ?int $limit = null, + ?int $offset = null + ); + + /** + * Finds a single object by a set of criteria. + * + * @param array $criteria The criteria. + * + * @return object|null The object. + * @psalm-return T|null + */ + public function findOneBy(array $criteria); + + /** + * Returns the class name of the object managed by the repository. + * + * @psalm-return class-string + */ + public function getClassName(); } diff --git a/lib/public/DB/ORM/IParameter.php b/lib/public/DB/ORM/IParameter.php new file mode 100644 index 0000000000000..00f3050f740c8 --- /dev/null +++ b/lib/public/DB/ORM/IParameter.php @@ -0,0 +1,37 @@ + Date: Wed, 6 Jul 2022 17:11:23 +0200 Subject: [PATCH 3/6] fixup! Introduce Doctrine ORM --- lib/private/DB/ORM/ParameterAdapter.php | 34 ++++++++++++++ lib/private/DB/ORM/QueryAdapter.php | 47 +++++++++++++++++++ lib/public/DB/ORM/IQuery.php | 35 ++++++++++++-- lib/public/DB/ORM/NoResultException.php | 7 +++ .../DB/ORM/NonUniqueResultException.php | 7 +++ 5 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 lib/private/DB/ORM/ParameterAdapter.php create mode 100644 lib/public/DB/ORM/NoResultException.php create mode 100644 lib/public/DB/ORM/NonUniqueResultException.php diff --git a/lib/private/DB/ORM/ParameterAdapter.php b/lib/private/DB/ORM/ParameterAdapter.php new file mode 100644 index 0000000000000..9927cb2145362 --- /dev/null +++ b/lib/private/DB/ORM/ParameterAdapter.php @@ -0,0 +1,34 @@ +parameter = $parameter; + } + + public function getName(): string { + return $this->parameter->getName(); + } + + public function getValue() { + return $this->parameter->getValue(); + } + + public function getType() { + return $this->parameter->getType(); + } + + public function setValue($value, $type = null): void { + $this->parameter->setValue($value, $type); + } + + public function typeWasSpecified(): bool { + return $this->parameter->typeWasSpecified(); + } +} diff --git a/lib/private/DB/ORM/QueryAdapter.php b/lib/private/DB/ORM/QueryAdapter.php index 6de031e00240a..5959a12f5b972 100644 --- a/lib/private/DB/ORM/QueryAdapter.php +++ b/lib/private/DB/ORM/QueryAdapter.php @@ -3,7 +3,11 @@ namespace OC\DB\ORM; use Doctrine\ORM\Query; +use OC\DB\QueryBuilder\Parameter; +use OCP\DB\ORM\IParameter; use OCP\DB\ORM\IQuery; +use OCP\DB\ORM\NonUniqueResultException; +use OCP\DB\ORM\NoResultException; class QueryAdapter implements IQuery { private Query $query; @@ -11,4 +15,47 @@ class QueryAdapter implements IQuery { public function __construct(Query $query) { $this->query = $query; } + + public function setCacheable(bool $cacheable): IQuery { + $this->query->setCacheable($cacheable); + return $this; + } + + public function isCacheable(): bool { + return $this->query->isCacheable(); + } + + public function getParameter($key): ?IParameter { + return new ParameterAdapter($this->query->getParameter($key)); + } + + public function setParameters($parameters): IQuery { + $this->query->setParameters($parameters); + } + + public function setParameter($key, $value, $type = null): IQuery { + $this->query->setParameter($key, $value, $type); + return $this; + } + + public function setMaxResults(?int $maxResults): IQuery { + $this->query->setMaxResults($maxResults); + return $this; + } + + public function getResult() { + return $this->query->getResult(); + } + + public function getOneOrNullResult() { + return $this->query->getOneOrNullResult(); + } + + public function getSingleResult() { + return $this->query->getSingleResult(); + } + + public function getSingleScalarResult() { + return $this->query->getSingleScalarResult(); + } } diff --git a/lib/public/DB/ORM/IQuery.php b/lib/public/DB/ORM/IQuery.php index 46091321d85ab..f84c5230d71e5 100644 --- a/lib/public/DB/ORM/IQuery.php +++ b/lib/public/DB/ORM/IQuery.php @@ -2,10 +2,6 @@ namespace OCP\DB\ORM; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\ORM\AbstractQuery; -use Doctrine\ORM\Query\Parameter; - interface IQuery { /** * Enable/disable second level query (result) caching for this query. @@ -45,7 +41,10 @@ public function setParameters($parameters): self; */ public function setParameter($key, $value, $type = null): self; - public function setMaxResults(): self; + /** + * Sets the maximum number of results to retrieve (the "limit"). + */ + public function setMaxResults(?int $maxResults): self; /** * Gets the list of results for the query. @@ -54,4 +53,30 @@ public function setMaxResults(): self; */ public function getResult(); + /** + * Get exactly one result or null. + * + * @return mixed + * + * @throws NonUniqueResultException + */ + public function getOneOrNullResult(); + + /** + * Gets the single result of the query. + * + * Enforces the presence as well as the uniqueness of the result. + * + * If the result is not unique, a NonUniqueResultException is thrown. + * If there is no result, a NoResultException is thrown. + * + * @return mixed + * + * @throws NonUniqueResultException If the query result is not unique. + * @throws NoResultException If the query returned no result. + */ + public function getSingleResult(); + + public function getSingleScalarResult(); + } diff --git a/lib/public/DB/ORM/NoResultException.php b/lib/public/DB/ORM/NoResultException.php new file mode 100644 index 0000000000000..e6da628c16795 --- /dev/null +++ b/lib/public/DB/ORM/NoResultException.php @@ -0,0 +1,7 @@ + Date: Mon, 11 Jul 2022 12:03:16 +0200 Subject: [PATCH 4/6] fixup! Introduce Doctrine ORM --- lib/composer/composer/autoload_classmap.php | 15 + lib/composer/composer/autoload_static.php | 15 + .../DB/ORM/EntityRepositoryAdapter.php | 10 +- lib/private/DB/ORM/QueryAdapter.php | 41 +- lib/public/DB/ORM/IEntityManager.php | 7 + lib/public/DB/ORM/IEntityRepository.php | 16 +- lib/public/DB/ORM/IParameter.php | 4 +- lib/public/DB/ORM/IQuery.php | 11 + lib/public/DB/ORM/IQueryBuilder.php | 553 ++++++++++++++++++ lib/public/IContainer.php | 1 - 10 files changed, 653 insertions(+), 20 deletions(-) create mode 100644 lib/public/DB/ORM/IQueryBuilder.php diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 004c569d21bec..5f3e39a1240d4 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -178,6 +178,15 @@ 'OCP\\DB\\IPreparedStatement' => $baseDir . '/lib/public/DB/IPreparedStatement.php', 'OCP\\DB\\IResult' => $baseDir . '/lib/public/DB/IResult.php', 'OCP\\DB\\ISchemaWrapper' => $baseDir . '/lib/public/DB/ISchemaWrapper.php', + 'OCP\\DB\\ORM\\IEntityManager' => $baseDir . '/lib/public/DB/ORM/IEntityManager.php', + 'OCP\\DB\\ORM\\IEntityRepository' => $baseDir . '/lib/public/DB/ORM/IEntityRepository.php', + 'OCP\\DB\\ORM\\IParameter' => $baseDir . '/lib/public/DB/ORM/IParameter.php', + 'OCP\\DB\\ORM\\IQuery' => $baseDir . '/lib/public/DB/ORM/IQuery.php', + 'OCP\\DB\\ORM\\LockMode' => $baseDir . '/lib/public/DB/ORM/LockMode.php', + 'OCP\\DB\\ORM\\NoResultException' => $baseDir . '/lib/public/DB/ORM/NoResultException.php', + 'OCP\\DB\\ORM\\NonUniqueResultException' => $baseDir . '/lib/public/DB/ORM/NonUniqueResultException.php', + 'OCP\\DB\\ORM\\OptimisticLockException' => $baseDir . '/lib/public/DB/ORM/OptimisticLockException.php', + 'OCP\\DB\\ORM\\PessimisticLockException' => $baseDir . '/lib/public/DB/ORM/PessimisticLockException.php', 'OCP\\DB\\QueryBuilder\\ICompositeExpression' => $baseDir . '/lib/public/DB/QueryBuilder/ICompositeExpression.php', 'OCP\\DB\\QueryBuilder\\IExpressionBuilder' => $baseDir . '/lib/public/DB/QueryBuilder/IExpressionBuilder.php', 'OCP\\DB\\QueryBuilder\\IFunctionBuilder' => $baseDir . '/lib/public/DB/QueryBuilder/IFunctionBuilder.php', @@ -1066,6 +1075,12 @@ 'OC\\DB\\MySQLMigrator' => $baseDir . '/lib/private/DB/MySQLMigrator.php', 'OC\\DB\\MySqlTools' => $baseDir . '/lib/private/DB/MySqlTools.php', 'OC\\DB\\OCSqlitePlatform' => $baseDir . '/lib/private/DB/OCSqlitePlatform.php', + 'OC\\DB\\ORM\\EntityManagerAdapter' => $baseDir . '/lib/private/DB/ORM/EntityManagerAdapter.php', + 'OC\\DB\\ORM\\EntityRepositoryAdapter' => $baseDir . '/lib/private/DB/ORM/EntityRepositoryAdapter.php', + 'OC\\DB\\ORM\\ParameterAdapter' => $baseDir . '/lib/private/DB/ORM/ParameterAdapter.php', + 'OC\\DB\\ORM\\Psr6CacheAdapter' => $baseDir . '/lib/private/DB/ORM/Psr6CacheAdapter.php', + 'OC\\DB\\ORM\\QueryAdapter' => $baseDir . '/lib/private/DB/ORM/QueryAdapter.php', + 'OC\\DB\\ORM\\TablePrefix' => $baseDir . '/lib/private/DB/ORM/TablePrefix.php', 'OC\\DB\\ObjectParameter' => $baseDir . '/lib/private/DB/ObjectParameter.php', 'OC\\DB\\OracleConnection' => $baseDir . '/lib/private/DB/OracleConnection.php', 'OC\\DB\\OracleMigrator' => $baseDir . '/lib/private/DB/OracleMigrator.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 8c13e047b3eee..433631000c0f0 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -211,6 +211,15 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\DB\\IPreparedStatement' => __DIR__ . '/../../..' . '/lib/public/DB/IPreparedStatement.php', 'OCP\\DB\\IResult' => __DIR__ . '/../../..' . '/lib/public/DB/IResult.php', 'OCP\\DB\\ISchemaWrapper' => __DIR__ . '/../../..' . '/lib/public/DB/ISchemaWrapper.php', + 'OCP\\DB\\ORM\\IEntityManager' => __DIR__ . '/../../..' . '/lib/public/DB/ORM/IEntityManager.php', + 'OCP\\DB\\ORM\\IEntityRepository' => __DIR__ . '/../../..' . '/lib/public/DB/ORM/IEntityRepository.php', + 'OCP\\DB\\ORM\\IParameter' => __DIR__ . '/../../..' . '/lib/public/DB/ORM/IParameter.php', + 'OCP\\DB\\ORM\\IQuery' => __DIR__ . '/../../..' . '/lib/public/DB/ORM/IQuery.php', + 'OCP\\DB\\ORM\\LockMode' => __DIR__ . '/../../..' . '/lib/public/DB/ORM/LockMode.php', + 'OCP\\DB\\ORM\\NoResultException' => __DIR__ . '/../../..' . '/lib/public/DB/ORM/NoResultException.php', + 'OCP\\DB\\ORM\\NonUniqueResultException' => __DIR__ . '/../../..' . '/lib/public/DB/ORM/NonUniqueResultException.php', + 'OCP\\DB\\ORM\\OptimisticLockException' => __DIR__ . '/../../..' . '/lib/public/DB/ORM/OptimisticLockException.php', + 'OCP\\DB\\ORM\\PessimisticLockException' => __DIR__ . '/../../..' . '/lib/public/DB/ORM/PessimisticLockException.php', 'OCP\\DB\\QueryBuilder\\ICompositeExpression' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/ICompositeExpression.php', 'OCP\\DB\\QueryBuilder\\IExpressionBuilder' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/IExpressionBuilder.php', 'OCP\\DB\\QueryBuilder\\IFunctionBuilder' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/IFunctionBuilder.php', @@ -1099,6 +1108,12 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\DB\\MySQLMigrator' => __DIR__ . '/../../..' . '/lib/private/DB/MySQLMigrator.php', 'OC\\DB\\MySqlTools' => __DIR__ . '/../../..' . '/lib/private/DB/MySqlTools.php', 'OC\\DB\\OCSqlitePlatform' => __DIR__ . '/../../..' . '/lib/private/DB/OCSqlitePlatform.php', + 'OC\\DB\\ORM\\EntityManagerAdapter' => __DIR__ . '/../../..' . '/lib/private/DB/ORM/EntityManagerAdapter.php', + 'OC\\DB\\ORM\\EntityRepositoryAdapter' => __DIR__ . '/../../..' . '/lib/private/DB/ORM/EntityRepositoryAdapter.php', + 'OC\\DB\\ORM\\ParameterAdapter' => __DIR__ . '/../../..' . '/lib/private/DB/ORM/ParameterAdapter.php', + 'OC\\DB\\ORM\\Psr6CacheAdapter' => __DIR__ . '/../../..' . '/lib/private/DB/ORM/Psr6CacheAdapter.php', + 'OC\\DB\\ORM\\QueryAdapter' => __DIR__ . '/../../..' . '/lib/private/DB/ORM/QueryAdapter.php', + 'OC\\DB\\ORM\\TablePrefix' => __DIR__ . '/../../..' . '/lib/private/DB/ORM/TablePrefix.php', 'OC\\DB\\ObjectParameter' => __DIR__ . '/../../..' . '/lib/private/DB/ObjectParameter.php', 'OC\\DB\\OracleConnection' => __DIR__ . '/../../..' . '/lib/private/DB/OracleConnection.php', 'OC\\DB\\OracleMigrator' => __DIR__ . '/../../..' . '/lib/private/DB/OracleMigrator.php', diff --git a/lib/private/DB/ORM/EntityRepositoryAdapter.php b/lib/private/DB/ORM/EntityRepositoryAdapter.php index b86baed4139f6..24e17d1f19b73 100644 --- a/lib/private/DB/ORM/EntityRepositoryAdapter.php +++ b/lib/private/DB/ORM/EntityRepositoryAdapter.php @@ -13,23 +13,23 @@ public function __construct(EntityRepository $entityRepository) { $this->entityRepository = $entityRepository; } - public function find($id) { + public function find($id): ?object { return $this->entityRepository->find($id); } - public function findAll() { + public function findAll(): array { return $this->entityRepository->findAll(); } - public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null) { + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array { return $this->entityRepository->findBy($criteria, $orderBy, $limit, $offset); } - public function findOneBy(array $criteria) { + public function findOneBy(array $criteria): ?object { return $this->entityRepository->findOneBy($criteria); } - public function getClassName() { + public function getClassName(): string { return $this->entityRepository->getClassName(); } } diff --git a/lib/private/DB/ORM/QueryAdapter.php b/lib/private/DB/ORM/QueryAdapter.php index 5959a12f5b972..3c68600ad3ac0 100644 --- a/lib/private/DB/ORM/QueryAdapter.php +++ b/lib/private/DB/ORM/QueryAdapter.php @@ -2,6 +2,7 @@ namespace OC\DB\ORM; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Query; use OC\DB\QueryBuilder\Parameter; use OCP\DB\ORM\IParameter; @@ -26,11 +27,13 @@ public function isCacheable(): bool { } public function getParameter($key): ?IParameter { - return new ParameterAdapter($this->query->getParameter($key)); + $internal = $this->query->getParameter($key); + return $internal === null ? new ParameterAdapter($internal) : null; } public function setParameters($parameters): IQuery { $this->query->setParameters($parameters); + return $this; } public function setParameter($key, $value, $type = null): IQuery { @@ -47,15 +50,47 @@ public function getResult() { return $this->query->getResult(); } + public function getArrayResult() { + return $this->query->getArrayResult(); + } + public function getOneOrNullResult() { return $this->query->getOneOrNullResult(); } public function getSingleResult() { - return $this->query->getSingleResult(); + try { + return $this->query->getSingleResult(); + } catch (\Doctrine\ORM\NoResultException $e) { + throw new NoResultException($e); + } catch (\Doctrine\ORM\NonUniqueResultException $e) { + throw new NonUniqueResultException($e); + } } public function getSingleScalarResult() { - return $this->query->getSingleScalarResult(); + try { + return $this->query->getSingleScalarResult(); + } catch (\Doctrine\ORM\NoResultException $e) { + throw new NoResultException($e); + } catch (\Doctrine\ORM\NonUniqueResultException $e) { + throw new NonUniqueResultException($e); + } + } + + public function getSql(): string { + return $this->query->getSQL(); + } + + /** + * Get all defined parameters. + * + * @return ArrayCollection The defined query parameters. + * @psalm-return ArrayCollection + */ + public function getParameters(): ArrayCollection + { + return $this->query->getParameters() + ->map(fn (Query\Parameter $parameter) => new Parameter($parameter)); } } diff --git a/lib/public/DB/ORM/IEntityManager.php b/lib/public/DB/ORM/IEntityManager.php index 355c743b6043e..9c44af63a64d8 100644 --- a/lib/public/DB/ORM/IEntityManager.php +++ b/lib/public/DB/ORM/IEntityManager.php @@ -1,5 +1,10 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + namespace OCP\DB\ORM; use DateTimeInterface; @@ -17,6 +22,8 @@ interface IEntityManager { */ public function createQuery(string $dql = ''): IQuery; + public function createQueryBuilder(): IQueryBuilder; + /** * Flushes all changes to objects that have been queued up to now to the database. * This effectively synchronizes the in-memory state of managed objects with the diff --git a/lib/public/DB/ORM/IEntityRepository.php b/lib/public/DB/ORM/IEntityRepository.php index 3ec5f72b1f728..2d43932a6139b 100644 --- a/lib/public/DB/ORM/IEntityRepository.php +++ b/lib/public/DB/ORM/IEntityRepository.php @@ -7,8 +7,7 @@ * * @template-covariant T of object */ -interface IEntityRepository -{ +interface IEntityRepository { /** * Finds an object by its primary key / identifier. * @@ -17,7 +16,7 @@ interface IEntityRepository * @return object|null The object. * @psalm-return T|null */ - public function find($id); + public function find($id): ?object; /** * Finds all objects in the repository. @@ -25,7 +24,7 @@ public function find($id); * @return array The objects. * @psalm-return T[] */ - public function findAll(); + public function findAll(): array; /** * Finds objects by a set of criteria. @@ -48,22 +47,21 @@ public function findBy( ?array $orderBy = null, ?int $limit = null, ?int $offset = null - ); + ): array; /** * Finds a single object by a set of criteria. * * @param array $criteria The criteria. * - * @return object|null The object. - * @psalm-return T|null + * @return T|null */ - public function findOneBy(array $criteria); + public function findOneBy(array $criteria): ?object; /** * Returns the class name of the object managed by the repository. * * @psalm-return class-string */ - public function getClassName(); + public function getClassName(): string; } diff --git a/lib/public/DB/ORM/IParameter.php b/lib/public/DB/ORM/IParameter.php index 00f3050f740c8..3fc7880ec0aa2 100644 --- a/lib/public/DB/ORM/IParameter.php +++ b/lib/public/DB/ORM/IParameter.php @@ -7,7 +7,7 @@ interface IParameter { /** * Retrieves the Parameter name. */ - public function getName(): string + public function getName(): string; /** * Retrieves the Parameter value. @@ -31,7 +31,7 @@ public function getType(); * * @return void */ - public function setValue($value, $type = null): void + public function setValue($value, $type = null): void; public function typeWasSpecified(): bool; } diff --git a/lib/public/DB/ORM/IQuery.php b/lib/public/DB/ORM/IQuery.php index f84c5230d71e5..4c0477ace5675 100644 --- a/lib/public/DB/ORM/IQuery.php +++ b/lib/public/DB/ORM/IQuery.php @@ -2,6 +2,8 @@ namespace OCP\DB\ORM; +use Doctrine\Common\Collections\ArrayCollection; + interface IQuery { /** * Enable/disable second level query (result) caching for this query. @@ -79,4 +81,13 @@ public function getSingleResult(); public function getSingleScalarResult(); + public function getSql(): string; + + /** + * Get all defined parameters. + * + * @return ArrayCollection The defined query parameters. + * @psalm-return ArrayCollection + */ + public function getParameters(): ArrayCollection; } diff --git a/lib/public/DB/ORM/IQueryBuilder.php b/lib/public/DB/ORM/IQueryBuilder.php new file mode 100644 index 0000000000000..8a5106c9c8047 --- /dev/null +++ b/lib/public/DB/ORM/IQueryBuilder.php @@ -0,0 +1,553 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OCP\DB\ORM; + +use DateTimeInterface; +use OCP\IDBConnection; + +/** + * @since 25.0.0 + */ +interface IQueryBuilder { + /** + * Gets an ExpressionBuilder used for object-oriented construction of query expressions. + * This producer method is intended for convenient inline usage. Example: + * + * + * $qb = $em->createQueryBuilder(); + * $qb + * ->select('u') + * ->from('User', 'u') + * ->where($qb->expr()->eq('u.id', 1)); + * + * + * For more complex expression construction, consider storing the expression + * builder object in a local variable. + */ + public function expr(): Query\Expr; + + /** + * Sets a query parameter for the query being constructed. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->where('u.id = :user_id') + * ->setParameter('user_id', 1); + * + * + * @param string|int $key The parameter position or name. + * @param mixed $value The parameter value. + * @param string|int|null $type ParameterType::* or \Doctrine\DBAL\Types\Type::* constant + */ + public function setParameter($key, $value, $type = null): self; + + /** + * Sets a collection of query parameters for the query being constructed. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->where('u.id = :user_id1 OR u.id = :user_id2') + * ->setParameters(new ArrayCollection(array( + * new Parameter('user_id1', 1), + * new Parameter('user_id2', 2) + * ))); + * + * + * @param ArrayCollection|mixed[] $parameters The query parameters to set. + * @psalm-param ArrayCollection|mixed[] $parameters + */ + public function setParameters($parameters): self; + + /** + * Gets all defined query parameters for the query being constructed. + * + * @return ArrayCollection The currently defined query parameters. + * @psalm-return ArrayCollection + */ + public function getParameters(); + + /** + * Gets a (previously set) query parameter of the query being constructed. + * + * @param string|int $key The key (index or name) of the bound parameter. + * + * @return Parameter|null The value of the bound parameter. + */ + public function getParameter($key): ?IParameter; + + /** + * Sets the position of the first result to retrieve (the "offset"). + * + * @param int|null $firstResult The first result to return. + * + * @return $this + */ + public function setFirstResult(?int $firstResult): self + + /** + * Gets the position of the first result the query object was set to retrieve (the "offset"). + * Returns NULL if {@link setFirstResult} was not applied to this QueryBuilder. + * + * @return int|null The position of the first result. + */ + public function getFirstResult(): ?int; + + /** + * Sets the maximum number of results to retrieve (the "limit"). + * + * @param int|null $maxResults The maximum number of results to retrieve. + */ + public function setMaxResults(?int $maxResults): self; + + /** + * Gets the maximum number of results the query object was set to retrieve (the "limit"). + * Returns NULL if {@link setMaxResults} was not applied to this query builder. + * + * @return int|null Maximum number of results. + */ + public function getMaxResults(): ?int; + + /** + * Specifies an item that is to be returned in the query result. + * Replaces any previously specified selections, if any. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u', 'p') + * ->from('User', 'u') + * ->leftJoin('u.Phonenumbers', 'p'); + * + * + * @param mixed $select The selection expressions. + */ + public function select($select = null): self; + + /** + * Adds a DISTINCT flag to this query. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->distinct() + * ->from('User', 'u'); + * + */ + public function distinct(bool $flag = true): self; + + /** + * Adds an item that is to be returned in the query result. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->addSelect('p') + * ->from('User', 'u') + * ->leftJoin('u.Phonenumbers', 'p'); + * + * + * @param mixed $select The selection expression. + */ + public function addSelect($select = null): self; + + /** + * Turns the query being built into a bulk delete query that ranges over + * a certain entity type. + * + * + * $qb = $em->createQueryBuilder() + * ->delete('User', 'u') + * ->where('u.id = :user_id') + * ->setParameter('user_id', 1); + * + * + * @param string|null $delete The class/type whose instances are subject to the deletion. + * @param string|null $alias The class/type alias used in the constructed query. + */ + public function delete(?string $delete = null, ?string $alias = null): self; + + /** + * Turns the query being built into a bulk update query that ranges over + * a certain entity type. + * + * + * $qb = $em->createQueryBuilder() + * ->update('User', 'u') + * ->set('u.password', '?1') + * ->where('u.id = ?2'); + * + * + * @param string|null $update The class/type whose instances are subject to the update. + * @param string|null $alias The class/type alias used in the constructed query. + */ + public function update(?string $update = null, ?string $alias = null): self; + + /** + * Creates and adds a query root corresponding to the entity identified by the given alias, + * forming a cartesian product with any existing query roots. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u'); + * + * + * @param string $from The class name. + * @param string $alias The alias of the class. + * @param string|null $indexBy The index for the from. + * + * @return $this + */ + public function from(string $from, string $alias, ?string $indexBy = null); + + /** + * Updates a query root corresponding to an entity setting its index by. This method is intended to be used with + * EntityRepository->createQueryBuilder(), which creates the initial FROM clause and do not allow you to update it + * setting an index by. + * + * + * $qb = $userRepository->createQueryBuilder('u') + * ->indexBy('u', 'u.id'); + * + * // Is equivalent to... + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u', 'u.id'); + * + * + * @param string $alias The root alias of the class. + * @param string $indexBy The index for the from. + * + * @throws Query\QueryException + */ + public function indexBy(string $alias, string $indexBy): self; + + /** + * Creates and adds a join over an entity association to the query. + * + * The entities in the joined association will be fetched as part of the query + * result if the alias used for the joined association is placed in the select + * expressions. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->join('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1'); + * + * + * @param string $join The relationship to join. + * @param string $alias The alias of the join. + * @param string|null $conditionType The condition type constant. Either ON or WITH. + * @param string|Expr\Comparison|Expr\Composite|null $condition The condition for the join. + * @param string|null $indexBy The index for the join. + * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType + */ + public function join(string $join, string $alias, ?string $conditionType = null, $condition = null, ?string $indexBy = null): self; + + /** + * Creates and adds a join over an entity association to the query. + * + * The entities in the joined association will be fetched as part of the query + * result if the alias used for the joined association is placed in the select + * expressions. + * + * [php] + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->innerJoin('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1'); + * + * @param string $join The relationship to join. + * @param string $alias The alias of the join. + * @param string|null $conditionType The condition type constant. Either ON or WITH. + * @param string|Expr\Comparison|Expr\Composite|null $condition The condition for the join. + * @param string|null $indexBy The index for the join. + * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType + */ + public function innerJoin(string $join, string $alias, ?string $conditionType = null, $condition = null, ?string $indexBy = null): self; + + /** + * Creates and adds a left join over an entity association to the query. + * + * The entities in the joined association will be fetched as part of the query + * result if the alias used for the joined association is placed in the select + * expressions. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->leftJoin('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1'); + * + * + * @param string $join The relationship to join. + * @param string $alias The alias of the join. + * @param string|null $conditionType The condition type constant. Either ON or WITH. + * @param string|Expr\Comparison|Expr\Composite|null $condition The condition for the join. + * @param string|null $indexBy The index for the join. + * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType + * + * @return $this + */ + public function leftJoin(string $join, string $alias, $conditionType = null, $condition = null, ?string $indexBy = null); + + /** + * Sets a new value for a field in a bulk update query. + * + * + * $qb = $em->createQueryBuilder() + * ->update('User', 'u') + * ->set('u.password', '?1') + * ->where('u.id = ?2'); + * + * + * @param string $key The key/field to set. + * @param mixed $value The value, expression, placeholder, etc. + */ + public function set(string $key, $value): self; + + /** + * Specifies one or more restrictions to the query result. + * Replaces any previously specified restrictions, if any. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->where('u.id = ?'); + * + * // You can optionally programmatically build and/or expressions + * $qb = $em->createQueryBuilder(); + * + * $or = $qb->expr()->orX(); + * $or->add($qb->expr()->eq('u.id', 1)); + * $or->add($qb->expr()->eq('u.id', 2)); + * + * $qb->update('User', 'u') + * ->set('u.password', '?') + * ->where($or); + * + * + * @param mixed $predicates The restriction predicates. + * + * @return $this + */ + public function where($predicates) + { + if (! (func_num_args() === 1 && $predicates instanceof Expr\Composite)) { + $predicates = new Expr\Andx(func_get_args()); + } + + return $this->add('where', $predicates); + } + + /** + * Adds one or more restrictions to the query results, forming a logical + * conjunction with any previously specified restrictions. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->where('u.username LIKE ?') + * ->andWhere('u.is_active = 1'); + * + * + * @see where() + * + * @param mixed $where The query restrictions. + * + * @return $this + */ + public function andWhere() + { + $args = func_get_args(); + $where = $this->getDQLPart('where'); + + if ($where instanceof Expr\Andx) { + $where->addMultiple($args); + } else { + array_unshift($args, $where); + $where = new Expr\Andx($args); + } + + return $this->add('where', $where); + } + + /** + * Adds one or more restrictions to the query results, forming a logical + * disjunction with any previously specified restrictions. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->where('u.id = 1') + * ->orWhere('u.id = 2'); + * + * + * @see where() + * + * @param mixed $where The WHERE statement. + * + * @return $this + */ + public function orWhere() + { + $args = func_get_args(); + $where = $this->getDQLPart('where'); + + if ($where instanceof Expr\Orx) { + $where->addMultiple($args); + } else { + array_unshift($args, $where); + $where = new Expr\Orx($args); + } + + return $this->add('where', $where); + } + + /** + * Specifies a grouping over the results of the query. + * Replaces any previously specified groupings, if any. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->groupBy('u.id'); + * + * + * @param string $groupBy The grouping expression. + * + * @return $this + */ + public function groupBy($groupBy) + { + return $this->add('groupBy', new Expr\GroupBy(func_get_args())); + } + + /** + * Adds a grouping expression to the query. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->groupBy('u.lastLogin') + * ->addGroupBy('u.createdAt'); + * + * + * @param string $groupBy The grouping expression. + * + * @return $this + */ + public function addGroupBy($groupBy) + { + return $this->add('groupBy', new Expr\GroupBy(func_get_args()), true); + } + + /** + * Specifies a restriction over the groups of the query. + * Replaces any previous having restrictions, if any. + * + * @param mixed $having The restriction over the groups. + * + * @return $this + */ + public function having($having) + { + if (! (func_num_args() === 1 && ($having instanceof Expr\Andx || $having instanceof Expr\Orx))) { + $having = new Expr\Andx(func_get_args()); + } + + return $this->add('having', $having); + } + + /** + * Adds a restriction over the groups of the query, forming a logical + * conjunction with any existing having restrictions. + * + * @param mixed $having The restriction to append. + * + * @return $this + */ + public function andHaving($having) + { + $args = func_get_args(); + $having = $this->getDQLPart('having'); + + if ($having instanceof Expr\Andx) { + $having->addMultiple($args); + } else { + array_unshift($args, $having); + $having = new Expr\Andx($args); + } + + return $this->add('having', $having); + } + + /** + * Adds a restriction over the groups of the query, forming a logical + * disjunction with any existing having restrictions. + * + * @param mixed $having The restriction to add. + * + * @return $this + */ + public function orHaving($having) + { + $args = func_get_args(); + $having = $this->getDQLPart('having'); + + if ($having instanceof Expr\Orx) { + $having->addMultiple($args); + } else { + array_unshift($args, $having); + $having = new Expr\Orx($args); + } + + return $this->add('having', $having); + } + + /** + * Specifies an ordering for the query results. + * Replaces any previously specified orderings, if any. + * + * @param string|Expr\OrderBy $sort The ordering expression. + * @param string|null $order The ordering direction. + * + * @return $this + */ + public function orderBy($sort, $order = null) + { + $orderBy = $sort instanceof Expr\OrderBy ? $sort : new Expr\OrderBy($sort, $order); + + return $this->add('orderBy', $orderBy); + } + + /** + * Adds an ordering to the query results. + * + * @param string|Expr\OrderBy $sort The ordering expression. + * @param string|null $order The ordering direction. + * + * @return $this + */ + public function addOrderBy($sort, $order = null) + { + $orderBy = $sort instanceof Expr\OrderBy ? $sort : new Expr\OrderBy($sort, $order); + + return $this->add('orderBy', $orderBy, true); + } +} diff --git a/lib/public/IContainer.php b/lib/public/IContainer.php index 08634ad508fd9..aaa2cafeb67fb 100644 --- a/lib/public/IContainer.php +++ b/lib/public/IContainer.php @@ -40,7 +40,6 @@ * IContainer is the basic interface to be used for any internal dependency injection mechanism * * @since 6.0.0 - * @deprecated 20.0.0 use \Psr\Container\ContainerInterface */ interface IContainer extends ContainerInterface { From f2e5d723d284b614c53a2d8d04cc0828a7ac428d Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 12 Aug 2022 11:10:28 +0200 Subject: [PATCH 5/6] fixup! Introduce Doctrine ORM --- lib/private/DB/ORM/EntityManagerAdapter.php | 4 + lib/private/DB/ORM/ExpressionAdapter.php | 25 + lib/private/DB/ORM/QueryBuilderAdapter.php | 527 ++++++++++++++++++++ lib/public/DB/ORM/IEntityManager.php | 5 + lib/public/DB/ORM/IQueryBuilder.php | 31 +- lib/public/DB/ORM/Query/IExpression.php | 12 + 6 files changed, 591 insertions(+), 13 deletions(-) create mode 100644 lib/private/DB/ORM/ExpressionAdapter.php create mode 100644 lib/private/DB/ORM/QueryBuilderAdapter.php create mode 100644 lib/public/DB/ORM/Query/IExpression.php diff --git a/lib/private/DB/ORM/EntityManagerAdapter.php b/lib/private/DB/ORM/EntityManagerAdapter.php index 3e702d788b554..bcfc936f094eb 100644 --- a/lib/private/DB/ORM/EntityManagerAdapter.php +++ b/lib/private/DB/ORM/EntityManagerAdapter.php @@ -35,6 +35,10 @@ public function __construct(ConnectionAdapter $connection) { $this->connection = $connection; } + public function createQueryBuilder(): IQueryBuilder { + return new QueryBuilderAdapter($this->em->createQueryBuilder()); + } + public function createQuery($dql = ''): IQuery { return new QueryAdapter($this->em->createQuery($dql)); diff --git a/lib/private/DB/ORM/ExpressionAdapter.php b/lib/private/DB/ORM/ExpressionAdapter.php new file mode 100644 index 0000000000000..1efd3aa4aaad7 --- /dev/null +++ b/lib/private/DB/ORM/ExpressionAdapter.php @@ -0,0 +1,25 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OC\DB\ORM; + +use Doctrine\Common\EventManager; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\ORMSetup; +use Doctrine\ORM\Query\Expr; +use OC\DB\ConnectionAdapter; +use OCP\DB\ORM\Query\IExpression; +use OCP\IDBConnection; + +class ExpressionAdapter implements IExpression { + private Expr $expr; + + public function __construct(Expr $expr) { + $this->expr = $expr; + } +} diff --git a/lib/private/DB/ORM/QueryBuilderAdapter.php b/lib/private/DB/ORM/QueryBuilderAdapter.php new file mode 100644 index 0000000000000..d7a979c08bc21 --- /dev/null +++ b/lib/private/DB/ORM/QueryBuilderAdapter.php @@ -0,0 +1,527 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OC\DB\ORM; + +use Doctrine\Common\EventManager; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\QueryBuilder; +use Doctrine\ORM\Query\Parameter; +use OC\DB\ConnectionAdapter; +use OCP\DB\ORM\IQueryBuilder; +use OCP\IDBConnection; + +class QueryBuilderAdapter implements IQueryBuilder { + private QueryBuilder $qb; + + public function __construct(QueryBuilder $queryBuilder) { + $this->qb = $queryBuilder; + } + + public function expr(): Query\IExpression { + return new ExpressionAdapter($this->qb->expr()); + } + + public function setParameter($key, $value, $type = null): self { + $this->qb->setParameter($key, $value, $type); + return $this; + } + + public function setParameters(array $parameters): self { + $ormParameters = [] + foreach ($parameters as $key => $value) { + $ormParameters[] = new Parameter($key, $value); + } + $this->qb->setParameters(new ArrayCollection($ormParameters)); + return $this; + } + + public function getParameters(): array { + $ormParameters = [] + foreach ($this->qb->getParameters() as $value) { + $ormParameters[] = new ParameterAdapter($value); + } + return $ormParameters; + } + + /** + * Gets a (previously set) query parameter of the query being constructed. + * + * @param string|int $key The key (index or name) of the bound parameter. + * + * @return ?IParameter The value of the bound parameter. + */ + public function getParameter($key): ?IParameter; + + /** + * Sets the position of the first result to retrieve (the "offset"). + * + * @param int|null $firstResult The first result to return. + * + * @return $this + */ + public function setFirstResult(?int $firstResult): self + + /** + * Gets the position of the first result the query object was set to retrieve (the "offset"). + * Returns NULL if {@link setFirstResult} was not applied to this QueryBuilder. + * + * @return int|null The position of the first result. + */ + public function getFirstResult(): ?int; + + /** + * Sets the maximum number of results to retrieve (the "limit"). + * + * @param int|null $maxResults The maximum number of results to retrieve. + */ + public function setMaxResults(?int $maxResults): self; + + /** + * Gets the maximum number of results the query object was set to retrieve (the "limit"). + * Returns NULL if {@link setMaxResults} was not applied to this query builder. + * + * @return int|null Maximum number of results. + */ + public function getMaxResults(): ?int; + + /** + * Specifies an item that is to be returned in the query result. + * Replaces any previously specified selections, if any. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u', 'p') + * ->from('User', 'u') + * ->leftJoin('u.Phonenumbers', 'p'); + * + * + * @param mixed $select The selection expressions. + */ + public function select($select = null): self; + + /** + * Adds a DISTINCT flag to this query. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->distinct() + * ->from('User', 'u'); + * + */ + public function distinct(bool $flag = true): self; + + /** + * Adds an item that is to be returned in the query result. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->addSelect('p') + * ->from('User', 'u') + * ->leftJoin('u.Phonenumbers', 'p'); + * + * + * @param mixed $select The selection expression. + */ + public function addSelect($select = null): self; + + /** + * Turns the query being built into a bulk delete query that ranges over + * a certain entity type. + * + * + * $qb = $em->createQueryBuilder() + * ->delete('User', 'u') + * ->where('u.id = :user_id') + * ->setParameter('user_id', 1); + * + * + * @param string|null $delete The class/type whose instances are subject to the deletion. + * @param string|null $alias The class/type alias used in the constructed query. + */ + public function delete(?string $delete = null, ?string $alias = null): self; + + /** + * Turns the query being built into a bulk update query that ranges over + * a certain entity type. + * + * + * $qb = $em->createQueryBuilder() + * ->update('User', 'u') + * ->set('u.password', '?1') + * ->where('u.id = ?2'); + * + * + * @param string|null $update The class/type whose instances are subject to the update. + * @param string|null $alias The class/type alias used in the constructed query. + */ + public function update(?string $update = null, ?string $alias = null): self; + + /** + * Creates and adds a query root corresponding to the entity identified by the given alias, + * forming a cartesian product with any existing query roots. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u'); + * + * + * @param string $from The class name. + * @param string $alias The alias of the class. + * @param string|null $indexBy The index for the from. + * + * @return $this + */ + public function from(string $from, string $alias, ?string $indexBy = null); + + /** + * Updates a query root corresponding to an entity setting its index by. This method is intended to be used with + * EntityRepository->createQueryBuilder(), which creates the initial FROM clause and do not allow you to update it + * setting an index by. + * + * + * $qb = $userRepository->createQueryBuilder('u') + * ->indexBy('u', 'u.id'); + * + * // Is equivalent to... + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u', 'u.id'); + * + * + * @param string $alias The root alias of the class. + * @param string $indexBy The index for the from. + * + * @throws Query\QueryException + */ + public function indexBy(string $alias, string $indexBy): self; + + /** + * Creates and adds a join over an entity association to the query. + * + * The entities in the joined association will be fetched as part of the query + * result if the alias used for the joined association is placed in the select + * expressions. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->join('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1'); + * + * + * @param string $join The relationship to join. + * @param string $alias The alias of the join. + * @param string|null $conditionType The condition type constant. Either ON or WITH. + * @param string|Expr\Comparison|Expr\Composite|null $condition The condition for the join. + * @param string|null $indexBy The index for the join. + * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType + */ + public function join(string $join, string $alias, ?string $conditionType = null, $condition = null, ?string $indexBy = null): self; + + /** + * Creates and adds a join over an entity association to the query. + * + * The entities in the joined association will be fetched as part of the query + * result if the alias used for the joined association is placed in the select + * expressions. + * + * [php] + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->innerJoin('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1'); + * + * @param string $join The relationship to join. + * @param string $alias The alias of the join. + * @param string|null $conditionType The condition type constant. Either ON or WITH. + * @param string|Expr\Comparison|Expr\Composite|null $condition The condition for the join. + * @param string|null $indexBy The index for the join. + * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType + */ + public function innerJoin(string $join, string $alias, ?string $conditionType = null, $condition = null, ?string $indexBy = null): self; + + /** + * Creates and adds a left join over an entity association to the query. + * + * The entities in the joined association will be fetched as part of the query + * result if the alias used for the joined association is placed in the select + * expressions. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->leftJoin('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1'); + * + * + * @param string $join The relationship to join. + * @param string $alias The alias of the join. + * @param string|null $conditionType The condition type constant. Either ON or WITH. + * @param string|Expr\Comparison|Expr\Composite|null $condition The condition for the join. + * @param string|null $indexBy The index for the join. + * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType + * + * @return $this + */ + public function leftJoin(string $join, string $alias, $conditionType = null, $condition = null, ?string $indexBy = null); + + /** + * Sets a new value for a field in a bulk update query. + * + * + * $qb = $em->createQueryBuilder() + * ->update('User', 'u') + * ->set('u.password', '?1') + * ->where('u.id = ?2'); + * + * + * @param string $key The key/field to set. + * @param mixed $value The value, expression, placeholder, etc. + */ + public function set(string $key, $value): self; + + /** + * Specifies one or more restrictions to the query result. + * Replaces any previously specified restrictions, if any. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->where('u.id = ?'); + * + * // You can optionally programmatically build and/or expressions + * $qb = $em->createQueryBuilder(); + * + * $or = $qb->expr()->orX(); + * $or->add($qb->expr()->eq('u.id', 1)); + * $or->add($qb->expr()->eq('u.id', 2)); + * + * $qb->update('User', 'u') + * ->set('u.password', '?') + * ->where($or); + * + * + * @param mixed $predicates The restriction predicates. + * + * @return $this + */ + public function where($predicates) + { + if (! (func_num_args() === 1 && $predicates instanceof Expr\Composite)) { + $predicates = new Expr\Andx(func_get_args()); + } + + return $this->add('where', $predicates); + } + + /** + * Adds one or more restrictions to the query results, forming a logical + * conjunction with any previously specified restrictions. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->where('u.username LIKE ?') + * ->andWhere('u.is_active = 1'); + * + * + * @see where() + * + * @param mixed $where The query restrictions. + * + * @return $this + */ + public function andWhere() + { + $args = func_get_args(); + $where = $this->getDQLPart('where'); + + if ($where instanceof Expr\Andx) { + $where->addMultiple($args); + } else { + array_unshift($args, $where); + $where = new Expr\Andx($args); + } + + return $this->add('where', $where); + } + + /** + * Adds one or more restrictions to the query results, forming a logical + * disjunction with any previously specified restrictions. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->where('u.id = 1') + * ->orWhere('u.id = 2'); + * + * + * @see where() + * + * @param mixed $where The WHERE statement. + * + * @return $this + */ + public function orWhere() + { + $args = func_get_args(); + $where = $this->getDQLPart('where'); + + if ($where instanceof Expr\Orx) { + $where->addMultiple($args); + } else { + array_unshift($args, $where); + $where = new Expr\Orx($args); + } + + return $this->add('where', $where); + } + + /** + * Specifies a grouping over the results of the query. + * Replaces any previously specified groupings, if any. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->groupBy('u.id'); + * + * + * @param string $groupBy The grouping expression. + * + * @return $this + */ + public function groupBy($groupBy) + { + return $this->add('groupBy', new Expr\GroupBy(func_get_args())); + } + + /** + * Adds a grouping expression to the query. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->groupBy('u.lastLogin') + * ->addGroupBy('u.createdAt'); + * + * + * @param string $groupBy The grouping expression. + * + * @return $this + */ + public function addGroupBy($groupBy) + { + return $this->add('groupBy', new Expr\GroupBy(func_get_args()), true); + } + + /** + * Specifies a restriction over the groups of the query. + * Replaces any previous having restrictions, if any. + * + * @param mixed $having The restriction over the groups. + * + * @return $this + */ + public function having($having) + { + if (! (func_num_args() === 1 && ($having instanceof Expr\Andx || $having instanceof Expr\Orx))) { + $having = new Expr\Andx(func_get_args()); + } + + return $this->add('having', $having); + } + + /** + * Adds a restriction over the groups of the query, forming a logical + * conjunction with any existing having restrictions. + * + * @param mixed $having The restriction to append. + * + * @return $this + */ + public function andHaving($having) + { + $args = func_get_args(); + $having = $this->getDQLPart('having'); + + if ($having instanceof Expr\Andx) { + $having->addMultiple($args); + } else { + array_unshift($args, $having); + $having = new Expr\Andx($args); + } + + return $this->add('having', $having); + } + + /** + * Adds a restriction over the groups of the query, forming a logical + * disjunction with any existing having restrictions. + * + * @param mixed $having The restriction to add. + * + * @return $this + */ + public function orHaving($having) + { + $args = func_get_args(); + $having = $this->getDQLPart('having'); + + if ($having instanceof Expr\Orx) { + $having->addMultiple($args); + } else { + array_unshift($args, $having); + $having = new Expr\Orx($args); + } + + return $this->add('having', $having); + } + + /** + * Specifies an ordering for the query results. + * Replaces any previously specified orderings, if any. + * + * @param string|Expr\OrderBy $sort The ordering expression. + * @param string|null $order The ordering direction. + * + * @return $this + */ + public function orderBy($sort, $order = null) + { + $orderBy = $sort instanceof Expr\OrderBy ? $sort : new Expr\OrderBy($sort, $order); + + return $this->add('orderBy', $orderBy); + } + + /** + * Adds an ordering to the query results. + * + * @param string|Expr\OrderBy $sort The ordering expression. + * @param string|null $order The ordering direction. + * + * @return $this + */ + public function addOrderBy($sort, $order = null) + { + $orderBy = $sort instanceof Expr\OrderBy ? $sort : new Expr\OrderBy($sort, $order); + + return $this->add('orderBy', $orderBy, true); + } +} diff --git a/lib/public/DB/ORM/IEntityManager.php b/lib/public/DB/ORM/IEntityManager.php index 9c44af63a64d8..9ce2e6e350c55 100644 --- a/lib/public/DB/ORM/IEntityManager.php +++ b/lib/public/DB/ORM/IEntityManager.php @@ -22,6 +22,11 @@ interface IEntityManager { */ public function createQuery(string $dql = ''): IQuery; + /** + * Creates a new QueryBuilder object. + * + * @since 25.0.0 + */ public function createQueryBuilder(): IQueryBuilder; /** diff --git a/lib/public/DB/ORM/IQueryBuilder.php b/lib/public/DB/ORM/IQueryBuilder.php index 8a5106c9c8047..52840d677cc44 100644 --- a/lib/public/DB/ORM/IQueryBuilder.php +++ b/lib/public/DB/ORM/IQueryBuilder.php @@ -28,8 +28,10 @@ interface IQueryBuilder { * * For more complex expression construction, consider storing the expression * builder object in a local variable. + * + * @since 25.0.0 */ - public function expr(): Query\Expr; + public function expr(): Query\IExpression; /** * Sets a query parameter for the query being constructed. @@ -56,31 +58,34 @@ public function setParameter($key, $value, $type = null): self; * ->select('u') * ->from('User', 'u') * ->where('u.id = :user_id1 OR u.id = :user_id2') - * ->setParameters(new ArrayCollection(array( - * new Parameter('user_id1', 1), - * new Parameter('user_id2', 2) - * ))); + * ->setParameters([ + * 'user_id1' => 1, + * 'user_id2', 2 + * ]); * * - * @param ArrayCollection|mixed[] $parameters The query parameters to set. - * @psalm-param ArrayCollection|mixed[] $parameters + * @param array $parameters The query parameters to set. + * @since 25.0.0 */ - public function setParameters($parameters): self; + public function setParameters(array $parameters): self; /** * Gets all defined query parameters for the query being constructed. * - * @return ArrayCollection The currently defined query parameters. - * @psalm-return ArrayCollection + * @return IParameter[] The currently defined query parameters. + * + * @since 25.0.0 */ - public function getParameters(); + public function getParameters(): array; /** * Gets a (previously set) query parameter of the query being constructed. * * @param string|int $key The key (index or name) of the bound parameter. * - * @return Parameter|null The value of the bound parameter. + * @return ?Parameter The value of the bound parameter. + * + * @since 25.0.0 */ public function getParameter($key): ?IParameter; @@ -89,7 +94,7 @@ public function getParameter($key): ?IParameter; * * @param int|null $firstResult The first result to return. * - * @return $this + * @since 25.0.0 */ public function setFirstResult(?int $firstResult): self diff --git a/lib/public/DB/ORM/Query/IExpression.php b/lib/public/DB/ORM/Query/IExpression.php new file mode 100644 index 0000000000000..f4d1b3364c26d --- /dev/null +++ b/lib/public/DB/ORM/Query/IExpression.php @@ -0,0 +1,12 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OCP\DB\ORM\Query; + +interface IExpression { + +} From 46012f188dd618689547d565db39f3332bde211e Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Wed, 24 Aug 2022 12:41:20 +0200 Subject: [PATCH 6/6] Update dependencies Signed-off-by: Carl Schwan --- 3rdparty | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdparty b/3rdparty index 020d0d3892bd3..09085cefc4085 160000 --- a/3rdparty +++ b/3rdparty @@ -1 +1 @@ -Subproject commit 020d0d3892bd3b7296db8ed21448c834d33d5723 +Subproject commit 09085cefc4085f8748eaef6fb1740e92fb0bf5f0