From bf6655a534a6775d30cafa67bd801276bda1d98d Mon Sep 17 00:00:00 2001 From: polo Date: Tue, 13 Aug 2024 23:45:21 +0200 Subject: =?UTF-8?q?VERSION=200.2=20doctrine=20ORM=20et=20entit=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Tools/AttachEntityListenersListener.php | 69 ++ .../Command/AbstractEntityManagerCommand.php | 25 + .../Command/ClearCache/CollectionRegionCommand.php | 119 +++ .../Command/ClearCache/EntityRegionCommand.php | 110 +++ .../Console/Command/ClearCache/MetadataCommand.php | 52 ++ .../Console/Command/ClearCache/QueryCommand.php | 54 ++ .../Command/ClearCache/QueryRegionCommand.php | 101 +++ .../Console/Command/ClearCache/ResultCommand.php | 65 ++ .../Console/Command/GenerateProxiesCommand.php | 96 +++ .../orm/src/Tools/Console/Command/InfoCommand.php | 80 ++ .../Console/Command/MappingDescribeCommand.php | 279 ++++++ .../src/Tools/Console/Command/RunDqlCommand.php | 118 +++ .../Console/Command/SchemaTool/AbstractCommand.php | 39 + .../Console/Command/SchemaTool/CreateCommand.php | 75 ++ .../Console/Command/SchemaTool/DropCommand.php | 116 +++ .../Console/Command/SchemaTool/UpdateCommand.php | 147 ++++ .../Console/Command/ValidateSchemaCommand.php | 89 ++ .../orm/src/Tools/Console/ConsoleRunner.php | 88 ++ .../src/Tools/Console/EntityManagerProvider.php | 14 + .../ConnectionFromManagerProvider.php | 26 + .../SingleManagerProvider.php | 31 + .../UnknownManagerException.php | 23 + .../orm/src/Tools/Console/MetadataFilter.php | 92 ++ vendor/doctrine/orm/src/Tools/Debug.php | 158 ++++ .../orm/src/Tools/DebugUnitOfWorkListener.php | 144 ++++ .../src/Tools/Event/GenerateSchemaEventArgs.php | 33 + .../Tools/Event/GenerateSchemaTableEventArgs.php | 40 + .../src/Tools/Exception/MissingColumnException.php | 23 + .../orm/src/Tools/Exception/NotSupported.php | 16 + .../orm/src/Tools/Pagination/CountOutputWalker.php | 125 +++ .../orm/src/Tools/Pagination/CountWalker.php | 68 ++ .../Exception/RowNumberOverFunctionNotEnabled.php | 16 + .../Tools/Pagination/LimitSubqueryOutputWalker.php | 544 ++++++++++++ .../src/Tools/Pagination/LimitSubqueryWalker.php | 155 ++++ .../orm/src/Tools/Pagination/Paginator.php | 263 ++++++ .../orm/src/Tools/Pagination/RootTypeWalker.php | 48 ++ .../src/Tools/Pagination/RowNumberOverFunction.php | 40 + .../orm/src/Tools/Pagination/WhereInWalker.php | 116 +++ .../orm/src/Tools/ResolveTargetEntityListener.php | 117 +++ vendor/doctrine/orm/src/Tools/SchemaTool.php | 932 +++++++++++++++++++++ vendor/doctrine/orm/src/Tools/SchemaValidator.php | 443 ++++++++++ vendor/doctrine/orm/src/Tools/ToolEvents.php | 23 + vendor/doctrine/orm/src/Tools/ToolsException.php | 24 + 43 files changed, 5236 insertions(+) create mode 100644 vendor/doctrine/orm/src/Tools/AttachEntityListenersListener.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/AbstractEntityManagerCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/CollectionRegionCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/EntityRegionCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/MetadataCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryRegionCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/ResultCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/GenerateProxiesCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/InfoCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/MappingDescribeCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/RunDqlCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/AbstractCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/CreateCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/DropCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/UpdateCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/ValidateSchemaCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/ConsoleRunner.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/ConnectionFromManagerProvider.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/SingleManagerProvider.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/UnknownManagerException.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/MetadataFilter.php create mode 100644 vendor/doctrine/orm/src/Tools/Debug.php create mode 100644 vendor/doctrine/orm/src/Tools/DebugUnitOfWorkListener.php create mode 100644 vendor/doctrine/orm/src/Tools/Event/GenerateSchemaEventArgs.php create mode 100644 vendor/doctrine/orm/src/Tools/Event/GenerateSchemaTableEventArgs.php create mode 100644 vendor/doctrine/orm/src/Tools/Exception/MissingColumnException.php create mode 100644 vendor/doctrine/orm/src/Tools/Exception/NotSupported.php create mode 100644 vendor/doctrine/orm/src/Tools/Pagination/CountOutputWalker.php create mode 100644 vendor/doctrine/orm/src/Tools/Pagination/CountWalker.php create mode 100644 vendor/doctrine/orm/src/Tools/Pagination/Exception/RowNumberOverFunctionNotEnabled.php create mode 100644 vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryOutputWalker.php create mode 100644 vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryWalker.php create mode 100644 vendor/doctrine/orm/src/Tools/Pagination/Paginator.php create mode 100644 vendor/doctrine/orm/src/Tools/Pagination/RootTypeWalker.php create mode 100644 vendor/doctrine/orm/src/Tools/Pagination/RowNumberOverFunction.php create mode 100644 vendor/doctrine/orm/src/Tools/Pagination/WhereInWalker.php create mode 100644 vendor/doctrine/orm/src/Tools/ResolveTargetEntityListener.php create mode 100644 vendor/doctrine/orm/src/Tools/SchemaTool.php create mode 100644 vendor/doctrine/orm/src/Tools/SchemaValidator.php create mode 100644 vendor/doctrine/orm/src/Tools/ToolEvents.php create mode 100644 vendor/doctrine/orm/src/Tools/ToolsException.php (limited to 'vendor/doctrine/orm/src/Tools') diff --git a/vendor/doctrine/orm/src/Tools/AttachEntityListenersListener.php b/vendor/doctrine/orm/src/Tools/AttachEntityListenersListener.php new file mode 100644 index 0000000..9203cfe --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/AttachEntityListenersListener.php @@ -0,0 +1,69 @@ +> + */ + private array $entityListeners = []; + + /** + * Adds an entity listener for a specific entity. + * + * @param class-string $entityClass The entity to attach the listener. + * @param class-string $listenerClass The listener class. + * @param Events::*|null $eventName The entity lifecycle event. + * @param non-falsy-string|null $listenerCallback The listener callback method or NULL to use $eventName. + */ + public function addEntityListener( + string $entityClass, + string $listenerClass, + string|null $eventName = null, + string|null $listenerCallback = null, + ): void { + $this->entityListeners[ltrim($entityClass, '\\')][] = [ + 'event' => $eventName, + 'class' => $listenerClass, + 'method' => $listenerCallback ?? $eventName, + ]; + } + + /** + * Processes event and attach the entity listener. + */ + public function loadClassMetadata(LoadClassMetadataEventArgs $event): void + { + $metadata = $event->getClassMetadata(); + + if (! isset($this->entityListeners[$metadata->name])) { + return; + } + + foreach ($this->entityListeners[$metadata->name] as $listener) { + if ($listener['event'] === null) { + EntityListenerBuilder::bindEntityListener($metadata, $listener['class']); + } else { + assert($listener['method'] !== null); + $metadata->addEntityListener($listener['event'], $listener['class'], $listener['method']); + } + } + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/AbstractEntityManagerCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/AbstractEntityManagerCommand.php new file mode 100644 index 0000000..370f4fb --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/AbstractEntityManagerCommand.php @@ -0,0 +1,25 @@ +getOption('em') === null + ? $this->entityManagerProvider->getDefaultManager() + : $this->entityManagerProvider->getManager($input->getOption('em')); + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/CollectionRegionCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/CollectionRegionCommand.php new file mode 100644 index 0000000..b4c6efa --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/CollectionRegionCommand.php @@ -0,0 +1,119 @@ +setName('orm:clear-cache:region:collection') + ->setDescription('Clear a second-level cache collection region') + ->addArgument('owner-class', InputArgument::OPTIONAL, 'The owner entity name.') + ->addArgument('association', InputArgument::OPTIONAL, 'The association collection name.') + ->addArgument('owner-id', InputArgument::OPTIONAL, 'The owner identifier.') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all entity regions will be deleted/invalidated.') + ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, all cache entries will be flushed.') + ->setHelp(<<<'EOT' +The %command.name% command is meant to clear a second-level cache collection regions for an associated Entity Manager. +It is possible to delete/invalidate all collection region, a specific collection region or flushes the cache provider. + +The execution type differ on how you execute the command. +If you want to invalidate all entries for an collection region this command would do the work: + +%command.name% 'Entities\MyEntity' 'collectionName' + +To invalidate a specific entry you should use : + +%command.name% 'Entities\MyEntity' 'collectionName' 1 + +If you want to invalidate all entries for the all collection regions: + +%command.name% --all + +Alternatively, if you want to flush the configured cache provider for an collection region use this command: + +%command.name% 'Entities\MyEntity' 'collectionName' --flush + +Finally, be aware that if --flush option is passed, +not all cache providers are able to flush entries, because of a limitation of its execution nature. +EOT); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $em = $this->getEntityManager($input); + $ownerClass = $input->getArgument('owner-class'); + $assoc = $input->getArgument('association'); + $ownerId = $input->getArgument('owner-id'); + $cache = $em->getCache(); + + if (! $cache instanceof Cache) { + throw new InvalidArgumentException('No second-level cache is configured on the given EntityManager.'); + } + + if (( ! $ownerClass || ! $assoc) && ! $input->getOption('all')) { + throw new InvalidArgumentException('Missing arguments "--owner-class" "--association"'); + } + + if ($input->getOption('flush')) { + $cache->getCollectionCacheRegion($ownerClass, $assoc) + ->evictAll(); + + $ui->comment( + sprintf( + 'Flushing cache provider configured for "%s#%s"', + $ownerClass, + $assoc, + ), + ); + + return 0; + } + + if ($input->getOption('all')) { + $ui->comment('Clearing all second-level cache collection regions'); + + $cache->evictEntityRegions(); + + return 0; + } + + if ($ownerId) { + $ui->comment( + sprintf( + 'Clearing second-level cache entry for collection "%s#%s" owner entity identified by "%s"', + $ownerClass, + $assoc, + $ownerId, + ), + ); + $cache->evictCollection($ownerClass, $assoc, $ownerId); + + return 0; + } + + $ui->comment(sprintf('Clearing second-level cache for collection "%s#%s"', $ownerClass, $assoc)); + $cache->evictCollectionRegion($ownerClass, $assoc); + + return 0; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/EntityRegionCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/EntityRegionCommand.php new file mode 100644 index 0000000..c5f2d65 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/EntityRegionCommand.php @@ -0,0 +1,110 @@ +setName('orm:clear-cache:region:entity') + ->setDescription('Clear a second-level cache entity region') + ->addArgument('entity-class', InputArgument::OPTIONAL, 'The entity name.') + ->addArgument('entity-id', InputArgument::OPTIONAL, 'The entity identifier.') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all entity regions will be deleted/invalidated.') + ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, all cache entries will be flushed.') + ->setHelp(<<<'EOT' +The %command.name% command is meant to clear a second-level cache entity region for an associated Entity Manager. +It is possible to delete/invalidate all entity region, a specific entity region or flushes the cache provider. + +The execution type differ on how you execute the command. +If you want to invalidate all entries for an entity region this command would do the work: + +%command.name% 'Entities\MyEntity' + +To invalidate a specific entry you should use : + +%command.name% 'Entities\MyEntity' 1 + +If you want to invalidate all entries for the all entity regions: + +%command.name% --all + +Alternatively, if you want to flush the configured cache provider for an entity region use this command: + +%command.name% 'Entities\MyEntity' --flush + +Finally, be aware that if --flush option is passed, +not all cache providers are able to flush entries, because of a limitation of its execution nature. +EOT); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $em = $this->getEntityManager($input); + $entityClass = $input->getArgument('entity-class'); + $entityId = $input->getArgument('entity-id'); + $cache = $em->getCache(); + + if (! $cache instanceof Cache) { + throw new InvalidArgumentException('No second-level cache is configured on the given EntityManager.'); + } + + if (! $entityClass && ! $input->getOption('all')) { + throw new InvalidArgumentException('Invalid argument "--entity-class"'); + } + + if ($input->getOption('flush')) { + $cache->getEntityCacheRegion($entityClass) + ->evictAll(); + + $ui->comment(sprintf('Flushing cache provider configured for entity named "%s"', $entityClass)); + + return 0; + } + + if ($input->getOption('all')) { + $ui->comment('Clearing all second-level cache entity regions'); + + $cache->evictEntityRegions(); + + return 0; + } + + if ($entityId) { + $ui->comment( + sprintf( + 'Clearing second-level cache entry for entity "%s" identified by "%s"', + $entityClass, + $entityId, + ), + ); + $cache->evictEntity($entityClass, $entityId); + + return 0; + } + + $ui->comment(sprintf('Clearing second-level cache for entity "%s"', $entityClass)); + $cache->evictEntityRegion($entityClass); + + return 0; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/MetadataCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/MetadataCommand.php new file mode 100644 index 0000000..147795b --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/MetadataCommand.php @@ -0,0 +1,52 @@ +setName('orm:clear-cache:metadata') + ->setDescription('Clear all metadata cache of the various cache drivers') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, cache entries will be flushed instead of deleted/invalidated.') + ->setHelp(<<<'EOT' +The %command.name% command is meant to clear the metadata cache of associated Entity Manager. +EOT); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $em = $this->getEntityManager($input); + $cacheDriver = $em->getConfiguration()->getMetadataCache(); + + if (! $cacheDriver) { + throw new InvalidArgumentException('No Metadata cache driver is configured on given EntityManager.'); + } + + $ui->comment('Clearing all Metadata cache entries'); + + $result = $cacheDriver->clear(); + $message = $result ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.'; + + $ui->success($message); + + return 0; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryCommand.php new file mode 100644 index 0000000..83edd7a --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryCommand.php @@ -0,0 +1,54 @@ +setName('orm:clear-cache:query') + ->setDescription('Clear all query cache of the various cache drivers') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->setHelp('The %command.name% command is meant to clear the query cache of associated Entity Manager.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $em = $this->getEntityManager($input); + $cache = $em->getConfiguration()->getQueryCache(); + + if (! $cache) { + throw new InvalidArgumentException('No Query cache driver is configured on given EntityManager.'); + } + + if ($cache instanceof ApcuAdapter) { + throw new LogicException('Cannot clear APCu Cache from Console, it\'s shared in the Webserver memory and not accessible from the CLI.'); + } + + $ui->comment('Clearing all Query cache entries'); + + $message = $cache->clear() ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.'; + + $ui->success($message); + + return 0; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryRegionCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryRegionCommand.php new file mode 100644 index 0000000..e80fb90 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryRegionCommand.php @@ -0,0 +1,101 @@ +setName('orm:clear-cache:region:query') + ->setDescription('Clear a second-level cache query region') + ->addArgument('region-name', InputArgument::OPTIONAL, 'The query region to clear.') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all query regions will be deleted/invalidated.') + ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, all cache entries will be flushed.') + ->setHelp(<<<'EOT' +The %command.name% command is meant to clear a second-level cache query region for an associated Entity Manager. +It is possible to delete/invalidate all query region, a specific query region or flushes the cache provider. + +The execution type differ on how you execute the command. +If you want to invalidate all entries for the default query region this command would do the work: + +%command.name% + +To invalidate entries for a specific query region you should use : + +%command.name% my_region_name + +If you want to invalidate all entries for the all query region: + +%command.name% --all + +Alternatively, if you want to flush the configured cache provider use this command: + +%command.name% my_region_name --flush + +Finally, be aware that if --flush option is passed, +not all cache providers are able to flush entries, because of a limitation of its execution nature. +EOT); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $em = $this->getEntityManager($input); + $name = $input->getArgument('region-name'); + $cache = $em->getCache(); + + if ($name === null) { + $name = Cache::DEFAULT_QUERY_REGION_NAME; + } + + if (! $cache instanceof Cache) { + throw new InvalidArgumentException('No second-level cache is configured on the given EntityManager.'); + } + + if ($input->getOption('flush')) { + $cache->getQueryCache($name) + ->getRegion() + ->evictAll(); + + $ui->comment( + sprintf( + 'Flushing cache provider configured for second-level cache query region named "%s"', + $name, + ), + ); + + return 0; + } + + if ($input->getOption('all')) { + $ui->comment('Clearing all second-level cache query regions'); + + $cache->evictQueryRegions(); + + return 0; + } + + $ui->comment(sprintf('Clearing second-level cache query region named "%s"', $name)); + $cache->evictQueryRegion($name); + + return 0; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/ResultCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/ResultCommand.php new file mode 100644 index 0000000..4f84e0b --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/ResultCommand.php @@ -0,0 +1,65 @@ +setName('orm:clear-cache:result') + ->setDescription('Clear all result cache of the various cache drivers') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, cache entries will be flushed instead of deleted/invalidated.') + ->setHelp(<<<'EOT' +The %command.name% command is meant to clear the result cache of associated Entity Manager. +It is possible to invalidate all cache entries at once - called delete -, or flushes the cache provider +instance completely. + +The execution type differ on how you execute the command. +If you want to invalidate the entries (and not delete from cache instance), this command would do the work: + +%command.name% + +Alternatively, if you want to flush the cache provider using this command: + +%command.name% --flush + +Finally, be aware that if --flush option is passed, not all cache providers are able to flush entries, +because of a limitation of its execution nature. +EOT); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $em = $this->getEntityManager($input); + $cache = $em->getConfiguration()->getResultCache(); + + if (! $cache) { + throw new InvalidArgumentException('No Result cache driver is configured on given EntityManager.'); + } + + $ui->comment('Clearing all Result cache entries'); + + $message = $cache->clear() ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.'; + + $ui->success($message); + + return 0; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/GenerateProxiesCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/GenerateProxiesCommand.php new file mode 100644 index 0000000..5a407de --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/GenerateProxiesCommand.php @@ -0,0 +1,96 @@ +setName('orm:generate-proxies') + ->setAliases(['orm:generate:proxies']) + ->setDescription('Generates proxy classes for entity classes') + ->addArgument('dest-path', InputArgument::OPTIONAL, 'The path to generate your proxy classes. If none is provided, it will attempt to grab from configuration.') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('filter', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A string pattern used to match entities that should be processed.') + ->setHelp('Generates proxy classes for entity classes.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $em = $this->getEntityManager($input); + + $metadatas = $em->getMetadataFactory()->getAllMetadata(); + $metadatas = MetadataFilter::filter($metadatas, $input->getOption('filter')); + + // Process destination directory + $destPath = $input->getArgument('dest-path'); + if ($destPath === null) { + $destPath = $em->getConfiguration()->getProxyDir(); + + if ($destPath === null) { + throw new InvalidArgumentException('Proxy directory cannot be null'); + } + } + + if (! is_dir($destPath)) { + mkdir($destPath, 0775, true); + } + + $destPath = realpath($destPath); + + if (! file_exists($destPath)) { + throw new InvalidArgumentException( + sprintf("Proxies destination directory '%s' does not exist.", $em->getConfiguration()->getProxyDir()), + ); + } + + if (! is_writable($destPath)) { + throw new InvalidArgumentException( + sprintf("Proxies destination directory '%s' does not have write permissions.", $destPath), + ); + } + + if (empty($metadatas)) { + $ui->success('No Metadata Classes to process.'); + + return 0; + } + + foreach ($metadatas as $metadata) { + $ui->text(sprintf('Processing entity "%s"', $metadata->name)); + } + + // Generating Proxies + $em->getProxyFactory()->generateProxyClasses($metadatas, $destPath); + + // Outputting information message + $ui->newLine(); + $ui->text(sprintf('Proxy classes generated to "%s"', $destPath)); + + return 0; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/InfoCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/InfoCommand.php new file mode 100644 index 0000000..deebb58 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/InfoCommand.php @@ -0,0 +1,80 @@ +setName('orm:info') + ->setDescription('Show basic information about all mapped entities') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->setHelp(<<<'EOT' +The %command.name% shows basic information about which +entities exist and possibly if their mapping information contains errors or +not. +EOT); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $entityManager = $this->getEntityManager($input); + + $entityClassNames = $entityManager->getConfiguration() + ->getMetadataDriverImpl() + ->getAllClassNames(); + + if (! $entityClassNames) { + $ui->caution( + [ + 'You do not have any mapped Doctrine ORM entities according to the current configuration.', + 'If you have entities or mapping files you should check your mapping configuration for errors.', + ], + ); + + return 1; + } + + $ui->text(sprintf('Found %d mapped entities:', count($entityClassNames))); + $ui->newLine(); + + $failure = false; + + foreach ($entityClassNames as $entityClassName) { + try { + $entityManager->getClassMetadata($entityClassName); + $ui->text(sprintf('[OK] %s', $entityClassName)); + } catch (MappingException $e) { + $ui->text( + [ + sprintf('[FAIL] %s', $entityClassName), + sprintf('%s', $e->getMessage()), + '', + ], + ); + + $failure = true; + } + } + + return $failure ? 1 : 0; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/MappingDescribeCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/MappingDescribeCommand.php new file mode 100644 index 0000000..41a177d --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/MappingDescribeCommand.php @@ -0,0 +1,279 @@ +setName('orm:mapping:describe') + ->addArgument('entityName', InputArgument::REQUIRED, 'Full or partial name of entity') + ->setDescription('Display information about mapped objects') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->setHelp(<<<'EOT' +The %command.full_name% command describes the metadata for the given full or partial entity class name. + + %command.full_name% My\Namespace\Entity\MyEntity + +Or: + + %command.full_name% MyEntity +EOT); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $entityManager = $this->getEntityManager($input); + + $this->displayEntity($input->getArgument('entityName'), $entityManager, $ui); + + return 0; + } + + /** + * Display all the mapping information for a single Entity. + * + * @param string $entityName Full or partial entity class name + */ + private function displayEntity( + string $entityName, + EntityManagerInterface $entityManager, + SymfonyStyle $ui, + ): void { + $metadata = $this->getClassMetadata($entityName, $entityManager); + + $ui->table( + ['Field', 'Value'], + array_merge( + [ + $this->formatField('Name', $metadata->name), + $this->formatField('Root entity name', $metadata->rootEntityName), + $this->formatField('Custom generator definition', $metadata->customGeneratorDefinition), + $this->formatField('Custom repository class', $metadata->customRepositoryClassName), + $this->formatField('Mapped super class?', $metadata->isMappedSuperclass), + $this->formatField('Embedded class?', $metadata->isEmbeddedClass), + $this->formatField('Parent classes', $metadata->parentClasses), + $this->formatField('Sub classes', $metadata->subClasses), + $this->formatField('Embedded classes', $metadata->subClasses), + $this->formatField('Identifier', $metadata->identifier), + $this->formatField('Inheritance type', $metadata->inheritanceType), + $this->formatField('Discriminator column', $metadata->discriminatorColumn), + $this->formatField('Discriminator value', $metadata->discriminatorValue), + $this->formatField('Discriminator map', $metadata->discriminatorMap), + $this->formatField('Generator type', $metadata->generatorType), + $this->formatField('Table', $metadata->table), + $this->formatField('Composite identifier?', $metadata->isIdentifierComposite), + $this->formatField('Foreign identifier?', $metadata->containsForeignIdentifier), + $this->formatField('Enum identifier?', $metadata->containsEnumIdentifier), + $this->formatField('Sequence generator definition', $metadata->sequenceGeneratorDefinition), + $this->formatField('Change tracking policy', $metadata->changeTrackingPolicy), + $this->formatField('Versioned?', $metadata->isVersioned), + $this->formatField('Version field', $metadata->versionField), + $this->formatField('Read only?', $metadata->isReadOnly), + + $this->formatEntityListeners($metadata->entityListeners), + ], + [$this->formatField('Association mappings:', '')], + $this->formatMappings($metadata->associationMappings), + [$this->formatField('Field mappings:', '')], + $this->formatMappings($metadata->fieldMappings), + ), + ); + } + + /** + * Return all mapped entity class names + * + * @return string[] + * @psalm-return class-string[] + */ + private function getMappedEntities(EntityManagerInterface $entityManager): array + { + $entityClassNames = $entityManager->getConfiguration() + ->getMetadataDriverImpl() + ->getAllClassNames(); + + if (! $entityClassNames) { + throw new InvalidArgumentException( + 'You do not have any mapped Doctrine ORM entities according to the current configuration. ' . + 'If you have entities or mapping files you should check your mapping configuration for errors.', + ); + } + + return $entityClassNames; + } + + /** + * Return the class metadata for the given entity + * name + * + * @param string $entityName Full or partial entity name + */ + private function getClassMetadata( + string $entityName, + EntityManagerInterface $entityManager, + ): ClassMetadata { + try { + return $entityManager->getClassMetadata($entityName); + } catch (MappingException) { + } + + $matches = array_filter( + $this->getMappedEntities($entityManager), + static fn ($mappedEntity) => preg_match('{' . preg_quote($entityName) . '}', $mappedEntity) + ); + + if (! $matches) { + throw new InvalidArgumentException(sprintf( + 'Could not find any mapped Entity classes matching "%s"', + $entityName, + )); + } + + if (count($matches) > 1) { + throw new InvalidArgumentException(sprintf( + 'Entity name "%s" is ambiguous, possible matches: "%s"', + $entityName, + implode(', ', $matches), + )); + } + + return $entityManager->getClassMetadata(current($matches)); + } + + /** + * Format the given value for console output + */ + private function formatValue(mixed $value): string + { + if ($value === '') { + return ''; + } + + if ($value === null) { + return 'Null'; + } + + if (is_bool($value)) { + return '' . ($value ? 'True' : 'False') . ''; + } + + if (empty($value)) { + return 'Empty'; + } + + if (is_array($value)) { + return json_encode( + $value, + JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR, + ); + } + + if (is_object($value)) { + return sprintf('<%s>', get_debug_type($value)); + } + + if (is_scalar($value)) { + return (string) $value; + } + + throw new InvalidArgumentException(sprintf('Do not know how to format value "%s"', print_r($value, true))); + } + + /** + * Add the given label and value to the two column table output + * + * @param string $label Label for the value + * @param mixed $value A Value to show + * + * @return string[] + * @psalm-return array{0: string, 1: string} + */ + private function formatField(string $label, mixed $value): array + { + if ($value === null) { + $value = 'None'; + } + + return [sprintf('%s', $label), $this->formatValue($value)]; + } + + /** + * Format the association mappings + * + * @psalm-param array $propertyMappings + * + * @return string[][] + * @psalm-return list + */ + private function formatMappings(array $propertyMappings): array + { + $output = []; + + foreach ($propertyMappings as $propertyName => $mapping) { + $output[] = $this->formatField(sprintf(' %s', $propertyName), ''); + + foreach ((array) $mapping as $field => $value) { + $output[] = $this->formatField(sprintf(' %s', $field), $this->formatValue($value)); + } + } + + return $output; + } + + /** + * Format the entity listeners + * + * @psalm-param list $entityListeners + * + * @return string[] + * @psalm-return array{0: string, 1: string} + */ + private function formatEntityListeners(array $entityListeners): array + { + return $this->formatField('Entity listeners', array_map('get_class', $entityListeners)); + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/RunDqlCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/RunDqlCommand.php new file mode 100644 index 0000000..252151e --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/RunDqlCommand.php @@ -0,0 +1,118 @@ +setName('orm:run-dql') + ->setDescription('Executes arbitrary DQL directly from the command line') + ->addArgument('dql', InputArgument::REQUIRED, 'The DQL to execute.') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('hydrate', null, InputOption::VALUE_REQUIRED, 'Hydration mode of result set. Should be either: object, array, scalar or single-scalar.', 'object') + ->addOption('first-result', null, InputOption::VALUE_REQUIRED, 'The first result in the result set.') + ->addOption('max-result', null, InputOption::VALUE_REQUIRED, 'The maximum number of results in the result set.') + ->addOption('depth', null, InputOption::VALUE_REQUIRED, 'Dumping depth of Entity graph.', 7) + ->addOption('show-sql', null, InputOption::VALUE_NONE, 'Dump generated SQL instead of executing query') + ->setHelp(<<<'EOT' + The %command.name% command executes the given DQL query and + outputs the results: + + php %command.full_name% "SELECT u FROM App\Entity\User u" + + You can also optionally specify some additional options like what type of + hydration to use when executing the query: + + php %command.full_name% "SELECT u FROM App\Entity\User u" --hydrate=array + + Additionally you can specify the first result and maximum amount of results to + show: + + php %command.full_name% "SELECT u FROM App\Entity\User u" --first-result=0 --max-result=30 + EOT); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = new SymfonyStyle($input, $output); + + $em = $this->getEntityManager($input); + + $dql = $input->getArgument('dql'); + if ($dql === null) { + throw new RuntimeException("Argument 'dql' is required in order to execute this command correctly."); + } + + $depth = $input->getOption('depth'); + + if (! is_numeric($depth)) { + throw new LogicException("Option 'depth' must contain an integer value"); + } + + $hydrationModeName = (string) $input->getOption('hydrate'); + $hydrationMode = 'Doctrine\ORM\Query::HYDRATE_' . strtoupper(str_replace('-', '_', $hydrationModeName)); + + if (! defined($hydrationMode)) { + throw new RuntimeException(sprintf( + "Hydration mode '%s' does not exist. It should be either: object. array, scalar or single-scalar.", + $hydrationModeName, + )); + } + + $query = $em->createQuery($dql); + + $firstResult = $input->getOption('first-result'); + if ($firstResult !== null) { + if (! is_numeric($firstResult)) { + throw new LogicException("Option 'first-result' must contain an integer value"); + } + + $query->setFirstResult((int) $firstResult); + } + + $maxResult = $input->getOption('max-result'); + if ($maxResult !== null) { + if (! is_numeric($maxResult)) { + throw new LogicException("Option 'max-result' must contain an integer value"); + } + + $query->setMaxResults((int) $maxResult); + } + + if ($input->getOption('show-sql')) { + $ui->text($query->getSQL()); + + return 0; + } + + $resultSet = $query->execute([], constant($hydrationMode)); + + $ui->text(Debug::dump($resultSet, (int) $input->getOption('depth'))); + + return 0; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/AbstractCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/AbstractCommand.php new file mode 100644 index 0000000..b1e4460 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/AbstractCommand.php @@ -0,0 +1,39 @@ +getEntityManager($input); + + $metadatas = $em->getMetadataFactory()->getAllMetadata(); + + if (empty($metadatas)) { + $ui->getErrorStyle()->success('No Metadata Classes to process.'); + + return 0; + } + + return $this->executeSchemaCommand($input, $output, new SchemaTool($em), $metadatas, $ui); + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/CreateCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/CreateCommand.php new file mode 100644 index 0000000..69e20c6 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/CreateCommand.php @@ -0,0 +1,75 @@ +setName('orm:schema-tool:create') + ->setDescription('Processes the schema and either create it directly on EntityManager Storage Connection or generate the SQL output') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Instead of trying to apply generated SQLs into EntityManager Storage Connection, output them.') + ->setHelp(<<<'EOT' +Processes the schema and either create it directly on EntityManager Storage Connection or generate the SQL output. + +Hint: If you have a database with tables that should not be managed +by the ORM, you can use a DBAL functionality to filter the tables and sequences down +on a global level: + + $config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool { + if ($assetName instanceof AbstractAsset) { + $assetName = $assetName->getName(); + } + + return !str_starts_with($assetName, 'audit_'); + }); +EOT); + } + + /** + * {@inheritDoc} + */ + protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui): int + { + $dumpSql = $input->getOption('dump-sql') === true; + + if ($dumpSql) { + $sqls = $schemaTool->getCreateSchemaSql($metadatas); + + foreach ($sqls as $sql) { + $ui->writeln(sprintf('%s;', $sql)); + } + + return 0; + } + + $notificationUi = $ui->getErrorStyle(); + + $notificationUi->caution('This operation should not be executed in a production environment!'); + + $notificationUi->text('Creating database schema...'); + $notificationUi->newLine(); + + $schemaTool->createSchema($metadatas); + + $notificationUi->success('Database schema created successfully!'); + + return 0; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/DropCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/DropCommand.php new file mode 100644 index 0000000..5c8253b --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/DropCommand.php @@ -0,0 +1,116 @@ +setName('orm:schema-tool:drop') + ->setDescription('Drop the complete database schema of EntityManager Storage Connection or generate the corresponding SQL output') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Instead of trying to apply generated SQLs into EntityManager Storage Connection, output them.') + ->addOption('force', 'f', InputOption::VALUE_NONE, "Don't ask for the deletion of the database, but force the operation to run.") + ->addOption('full-database', null, InputOption::VALUE_NONE, 'Instead of using the Class Metadata to detect the database table schema, drop ALL assets that the database contains.') + ->setHelp(<<<'EOT' +Processes the schema and either drop the database schema of EntityManager Storage Connection or generate the SQL output. +Beware that the complete database is dropped by this command, even tables that are not relevant to your metadata model. + +Hint: If you have a database with tables that should not be managed +by the ORM, you can use a DBAL functionality to filter the tables and sequences down +on a global level: + + $config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool { + if ($assetName instanceof AbstractAsset) { + $assetName = $assetName->getName(); + } + + return !str_starts_with($assetName, 'audit_'); + }); +EOT); + } + + /** + * {@inheritDoc} + */ + protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui): int + { + $isFullDatabaseDrop = $input->getOption('full-database'); + $dumpSql = $input->getOption('dump-sql') === true; + $force = $input->getOption('force') === true; + + if ($dumpSql) { + if ($isFullDatabaseDrop) { + $sqls = $schemaTool->getDropDatabaseSQL(); + } else { + $sqls = $schemaTool->getDropSchemaSQL($metadatas); + } + + foreach ($sqls as $sql) { + $ui->writeln(sprintf('%s;', $sql)); + } + + return 0; + } + + $notificationUi = $ui->getErrorStyle(); + + if ($force) { + $notificationUi->text('Dropping database schema...'); + $notificationUi->newLine(); + + if ($isFullDatabaseDrop) { + $schemaTool->dropDatabase(); + } else { + $schemaTool->dropSchema($metadatas); + } + + $notificationUi->success('Database schema dropped successfully!'); + + return 0; + } + + $notificationUi->caution('This operation should not be executed in a production environment!'); + + if ($isFullDatabaseDrop) { + $sqls = $schemaTool->getDropDatabaseSQL(); + } else { + $sqls = $schemaTool->getDropSchemaSQL($metadatas); + } + + if (empty($sqls)) { + $notificationUi->success('Nothing to drop. The database is empty!'); + + return 0; + } + + $notificationUi->text( + [ + sprintf('The Schema-Tool would execute "%s" queries to update the database.', count($sqls)), + '', + 'Please run the operation by passing one - or both - of the following options:', + '', + sprintf(' %s --force to execute the command', $this->getName()), + sprintf(' %s --dump-sql to dump the SQL statements to the screen', $this->getName()), + ], + ); + + return 1; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/UpdateCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/UpdateCommand.php new file mode 100644 index 0000000..f35fc38 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/UpdateCommand.php @@ -0,0 +1,147 @@ +setName($this->name) + ->setDescription('Executes (or dumps) the SQL needed to update the database schema to match the current mapping metadata') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('complete', null, InputOption::VALUE_NONE, 'This option is a no-op, is deprecated and will be removed in 4.0') + ->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Dumps the generated SQL statements to the screen (does not execute them).') + ->addOption('force', 'f', InputOption::VALUE_NONE, 'Causes the generated SQL statements to be physically executed against your database.') + ->setHelp(<<<'EOT' +The %command.name% command generates the SQL needed to +synchronize the database schema with the current mapping metadata of the +default entity manager. + +For example, if you add metadata for a new column to an entity, this command +would generate and output the SQL needed to add the new column to the database: + +%command.name% --dump-sql + +Alternatively, you can execute the generated queries: + +%command.name% --force + +If both options are specified, the queries are output and then executed: + +%command.name% --dump-sql --force + +Finally, be aware that this task will drop all database assets (e.g. tables, +etc) that are *not* described by the current metadata. In other words, without +this option, this task leaves untouched any "extra" tables that exist in the +database, but which aren't described by any metadata. + +Hint: If you have a database with tables that should not be managed +by the ORM, you can use a DBAL functionality to filter the tables and sequences down +on a global level: + + $config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool { + if ($assetName instanceof AbstractAsset) { + $assetName = $assetName->getName(); + } + + return !str_starts_with($assetName, 'audit_'); + }); +EOT); + } + + /** + * {@inheritDoc} + */ + protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui): int + { + $notificationUi = $ui->getErrorStyle(); + + if ($input->getOption('complete') === true) { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/pull/11354', + 'The --complete option is a no-op, is deprecated and will be removed in Doctrine ORM 4.0.', + ); + $notificationUi->warning('The --complete option is a no-op, is deprecated and will be removed in Doctrine ORM 4.0.'); + } + + $sqls = $schemaTool->getUpdateSchemaSql($metadatas); + + if (empty($sqls)) { + $notificationUi->success('Nothing to update - your database is already in sync with the current entity metadata.'); + + return 0; + } + + $dumpSql = $input->getOption('dump-sql') === true; + $force = $input->getOption('force') === true; + + if ($dumpSql) { + foreach ($sqls as $sql) { + $ui->writeln(sprintf('%s;', $sql)); + } + } + + if ($force) { + if ($dumpSql) { + $notificationUi->newLine(); + } + + $notificationUi->text('Updating database schema...'); + $notificationUi->newLine(); + + $schemaTool->updateSchema($metadatas); + + $pluralization = count($sqls) === 1 ? 'query was' : 'queries were'; + + $notificationUi->text(sprintf(' %s %s executed', count($sqls), $pluralization)); + $notificationUi->success('Database schema updated successfully!'); + } + + if ($dumpSql || $force) { + return 0; + } + + $notificationUi->caution( + [ + 'This operation should not be executed in a production environment!', + '', + 'Use the incremental update to detect changes during development and use', + 'the SQL DDL provided to manually update your database in production.', + ], + ); + + $notificationUi->text( + [ + sprintf('The Schema-Tool would execute "%s" queries to update the database.', count($sqls)), + '', + 'Please run the operation by passing one - or both - of the following options:', + '', + sprintf(' %s --force to execute the command', $this->getName()), + sprintf(' %s --dump-sql to dump the SQL statements to the screen', $this->getName()), + ], + ); + + return 1; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ValidateSchemaCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ValidateSchemaCommand.php new file mode 100644 index 0000000..cffb4ce --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/ValidateSchemaCommand.php @@ -0,0 +1,89 @@ +setName('orm:validate-schema') + ->setDescription('Validate the mapping files') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('skip-mapping', null, InputOption::VALUE_NONE, 'Skip the mapping validation check') + ->addOption('skip-sync', null, InputOption::VALUE_NONE, 'Skip checking if the mapping is in sync with the database') + ->addOption('skip-property-types', null, InputOption::VALUE_NONE, 'Skip checking if property types match the Doctrine types') + ->setHelp('Validate that the mapping files are correct and in sync with the database.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $em = $this->getEntityManager($input); + $validator = new SchemaValidator($em, ! $input->getOption('skip-property-types')); + $exit = 0; + + $ui->section('Mapping'); + + if ($input->getOption('skip-mapping')) { + $ui->text('[SKIPPED] The mapping was not checked.'); + } else { + $errors = $validator->validateMapping(); + if ($errors) { + foreach ($errors as $className => $errorMessages) { + $ui->text( + sprintf( + '[FAIL] The entity-class %s mapping is invalid:', + $className, + ), + ); + + $ui->listing($errorMessages); + $ui->newLine(); + } + + ++$exit; + } else { + $ui->success('The mapping files are correct.'); + } + } + + $ui->section('Database'); + + if ($input->getOption('skip-sync')) { + $ui->text('[SKIPPED] The database was not checked for synchronicity.'); + } elseif (! $validator->schemaInSyncWithMetadata()) { + $ui->error('The database schema is not in sync with the current mapping file.'); + + if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { + $sqls = $validator->getUpdateSchemaList(); + $ui->comment(sprintf('%d schema diff(s) detected:', count($sqls))); + foreach ($sqls as $sql) { + $ui->text(sprintf(' %s;', $sql)); + } + } + + $exit += 2; + } else { + $ui->success('The database schema is in sync with the mapping files.'); + } + + return $exit; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/ConsoleRunner.php b/vendor/doctrine/orm/src/Tools/Console/ConsoleRunner.php new file mode 100644 index 0000000..0a00483 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/ConsoleRunner.php @@ -0,0 +1,88 @@ +run(); + } + + /** + * Creates a console application with the given helperset and + * optional commands. + * + * @param SymfonyCommand[] $commands + * + * @throws OutOfBoundsException + */ + public static function createApplication( + EntityManagerProvider $entityManagerProvider, + array $commands = [], + ): Application { + $version = InstalledVersions::getVersion('doctrine/orm'); + assert($version !== null); + + $cli = new Application('Doctrine Command Line Interface', $version); + $cli->setCatchExceptions(true); + + self::addCommands($cli, $entityManagerProvider); + $cli->addCommands($commands); + + return $cli; + } + + public static function addCommands(Application $cli, EntityManagerProvider $entityManagerProvider): void + { + $connectionProvider = new ConnectionFromManagerProvider($entityManagerProvider); + + if (class_exists(DBALConsole\Command\ReservedWordsCommand::class)) { + $cli->add(new DBALConsole\Command\ReservedWordsCommand($connectionProvider)); + } + + $cli->addCommands( + [ + // DBAL Commands + new DBALConsole\Command\RunSqlCommand($connectionProvider), + + // ORM Commands + new Command\ClearCache\CollectionRegionCommand($entityManagerProvider), + new Command\ClearCache\EntityRegionCommand($entityManagerProvider), + new Command\ClearCache\MetadataCommand($entityManagerProvider), + new Command\ClearCache\QueryCommand($entityManagerProvider), + new Command\ClearCache\QueryRegionCommand($entityManagerProvider), + new Command\ClearCache\ResultCommand($entityManagerProvider), + new Command\SchemaTool\CreateCommand($entityManagerProvider), + new Command\SchemaTool\UpdateCommand($entityManagerProvider), + new Command\SchemaTool\DropCommand($entityManagerProvider), + new Command\GenerateProxiesCommand($entityManagerProvider), + new Command\RunDqlCommand($entityManagerProvider), + new Command\ValidateSchemaCommand($entityManagerProvider), + new Command\InfoCommand($entityManagerProvider), + new Command\MappingDescribeCommand($entityManagerProvider), + ], + ); + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider.php b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider.php new file mode 100644 index 0000000..866589b --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider.php @@ -0,0 +1,14 @@ +entityManagerProvider->getDefaultManager()->getConnection(); + } + + public function getConnection(string $name): Connection + { + return $this->entityManagerProvider->getManager($name)->getConnection(); + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/SingleManagerProvider.php b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/SingleManagerProvider.php new file mode 100644 index 0000000..ebe60c9 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/SingleManagerProvider.php @@ -0,0 +1,31 @@ +entityManager; + } + + public function getManager(string $name): EntityManagerInterface + { + if ($name !== $this->defaultManagerName) { + throw UnknownManagerException::unknownManager($name, [$this->defaultManagerName]); + } + + return $this->entityManager; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/UnknownManagerException.php b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/UnknownManagerException.php new file mode 100644 index 0000000..583d909 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/UnknownManagerException.php @@ -0,0 +1,23 @@ + $knownManagers */ + public static function unknownManager(string $unknownManager, array $knownManagers = []): self + { + return new self(sprintf( + 'Requested unknown entity manager: %s, known managers: %s', + $unknownManager, + implode(', ', $knownManagers), + )); + } +} diff --git a/vendor/doctrine/orm/src/Tools/Console/MetadataFilter.php b/vendor/doctrine/orm/src/Tools/Console/MetadataFilter.php new file mode 100644 index 0000000..05e248c --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/MetadataFilter.php @@ -0,0 +1,92 @@ +filter = (array) $filter; + + parent::__construct($metadata); + } + + public function accept(): bool + { + if (count($this->filter) === 0) { + return true; + } + + $it = $this->getInnerIterator(); + $metadata = $it->current(); + + foreach ($this->filter as $filter) { + $pregResult = preg_match('/' . $filter . '/', $metadata->getName()); + + if ($pregResult === false) { + throw new RuntimeException( + sprintf("Error while evaluating regex '/%s/'.", $filter), + ); + } + + if ($pregResult) { + return true; + } + } + + return false; + } + + /** @return ArrayIterator */ + public function getInnerIterator(): ArrayIterator + { + $innerIterator = parent::getInnerIterator(); + + assert($innerIterator instanceof ArrayIterator); + + return $innerIterator; + } + + public function count(): int + { + return count($this->getInnerIterator()); + } +} diff --git a/vendor/doctrine/orm/src/Tools/Debug.php b/vendor/doctrine/orm/src/Tools/Debug.php new file mode 100644 index 0000000..8521e53 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Debug.php @@ -0,0 +1,158 @@ +toArray(); + } + + if (! $maxDepth) { + return is_object($var) ? $var::class + : (is_array($var) ? 'Array(' . count($var) . ')' : $var); + } + + if (is_array($var)) { + $return = []; + + foreach ($var as $k => $v) { + $return[$k] = self::export($v, $maxDepth - 1); + } + + return $return; + } + + if (! is_object($var)) { + return $var; + } + + $return = new stdClass(); + if ($var instanceof DateTimeInterface) { + $return->__CLASS__ = $var::class; + $return->date = $var->format('c'); + $return->timezone = $var->getTimezone()->getName(); + + return $return; + } + + $return->__CLASS__ = DefaultProxyClassNameResolver::getClass($var); + + if ($var instanceof Proxy) { + $return->__IS_PROXY__ = true; + $return->__PROXY_INITIALIZED__ = $var->__isInitialized(); + } + + if ($var instanceof ArrayObject || $var instanceof ArrayIterator) { + $return->__STORAGE__ = self::export($var->getArrayCopy(), $maxDepth - 1); + } + + return self::fillReturnWithClassAttributes($var, $return, $maxDepth); + } + + /** + * Fill the $return variable with class attributes + * Based on obj2array function from {@see https://secure.php.net/manual/en/function.get-object-vars.php#47075} + */ + private static function fillReturnWithClassAttributes(object $var, stdClass $return, int $maxDepth): stdClass + { + $clone = (array) $var; + + foreach (array_keys($clone) as $key) { + $aux = explode("\0", (string) $key); + $name = end($aux); + if ($aux[0] === '') { + $name .= ':' . ($aux[1] === '*' ? 'protected' : $aux[1] . ':private'); + } + + $return->$name = self::export($clone[$key], $maxDepth - 1); + } + + return $return; + } +} diff --git a/vendor/doctrine/orm/src/Tools/DebugUnitOfWorkListener.php b/vendor/doctrine/orm/src/Tools/DebugUnitOfWorkListener.php new file mode 100644 index 0000000..71059f7 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/DebugUnitOfWorkListener.php @@ -0,0 +1,144 @@ +dumpIdentityMap($args->getObjectManager()); + } + + /** + * Dumps the contents of the identity map into a stream. + */ + public function dumpIdentityMap(EntityManagerInterface $em): void + { + $uow = $em->getUnitOfWork(); + $identityMap = $uow->getIdentityMap(); + + $fh = fopen($this->file, 'xb+'); + if (count($identityMap) === 0) { + fwrite($fh, 'Flush Operation [' . $this->context . "] - Empty identity map.\n"); + + return; + } + + fwrite($fh, 'Flush Operation [' . $this->context . "] - Dumping identity map:\n"); + foreach ($identityMap as $className => $map) { + fwrite($fh, 'Class: ' . $className . "\n"); + + foreach ($map as $entity) { + fwrite($fh, ' Entity: ' . $this->getIdString($entity, $uow) . ' ' . spl_object_id($entity) . "\n"); + fwrite($fh, " Associations:\n"); + + $cm = $em->getClassMetadata($className); + + foreach ($cm->associationMappings as $field => $assoc) { + fwrite($fh, ' ' . $field . ' '); + $value = $cm->getFieldValue($entity, $field); + + if ($assoc->isToOne()) { + if ($value === null) { + fwrite($fh, " NULL\n"); + } else { + if ($uow->isUninitializedObject($value)) { + fwrite($fh, '[PROXY] '); + } + + fwrite($fh, $this->getIdString($value, $uow) . ' ' . spl_object_id($value) . "\n"); + } + } else { + $initialized = ! ($value instanceof PersistentCollection) || $value->isInitialized(); + if ($value === null) { + fwrite($fh, " NULL\n"); + } elseif ($initialized) { + fwrite($fh, '[INITIALIZED] ' . $this->getType($value) . ' ' . count($value) . " elements\n"); + + foreach ($value as $obj) { + fwrite($fh, ' ' . $this->getIdString($obj, $uow) . ' ' . spl_object_id($obj) . "\n"); + } + } else { + fwrite($fh, '[PROXY] ' . $this->getType($value) . " unknown element size\n"); + foreach ($value->unwrap() as $obj) { + fwrite($fh, ' ' . $this->getIdString($obj, $uow) . ' ' . spl_object_id($obj) . "\n"); + } + } + } + } + } + } + + fclose($fh); + } + + private function getType(mixed $var): string + { + if (is_object($var)) { + $refl = new ReflectionObject($var); + + return $refl->getShortName(); + } + + return gettype($var); + } + + private function getIdString(object $entity, UnitOfWork $uow): string + { + if ($uow->isInIdentityMap($entity)) { + $ids = $uow->getEntityIdentifier($entity); + $idstring = ''; + + foreach ($ids as $k => $v) { + $idstring .= $k . '=' . $v; + } + } else { + $idstring = 'NEWOBJECT '; + } + + $state = $uow->getEntityState($entity); + + if ($state === UnitOfWork::STATE_NEW) { + $idstring .= ' [NEW]'; + } elseif ($state === UnitOfWork::STATE_REMOVED) { + $idstring .= ' [REMOVED]'; + } elseif ($state === UnitOfWork::STATE_MANAGED) { + $idstring .= ' [MANAGED]'; + } elseif ($state === UnitOfWork::STATE_DETACHED) { + $idstring .= ' [DETACHED]'; + } + + return $idstring; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Event/GenerateSchemaEventArgs.php b/vendor/doctrine/orm/src/Tools/Event/GenerateSchemaEventArgs.php new file mode 100644 index 0000000..3b0993e --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Event/GenerateSchemaEventArgs.php @@ -0,0 +1,33 @@ +em; + } + + public function getSchema(): Schema + { + return $this->schema; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Event/GenerateSchemaTableEventArgs.php b/vendor/doctrine/orm/src/Tools/Event/GenerateSchemaTableEventArgs.php new file mode 100644 index 0000000..a09aaae --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Event/GenerateSchemaTableEventArgs.php @@ -0,0 +1,40 @@ +classMetadata; + } + + public function getSchema(): Schema + { + return $this->schema; + } + + public function getClassTable(): Table + { + return $this->classTable; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Exception/MissingColumnException.php b/vendor/doctrine/orm/src/Tools/Exception/MissingColumnException.php new file mode 100644 index 0000000..764721e --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Exception/MissingColumnException.php @@ -0,0 +1,23 @@ + FROM ()) + * + * Works with composite keys but cannot deal with queries that have multiple + * root entities (e.g. `SELECT f, b from Foo, Bar`) + * + * Note that the ORDER BY clause is not removed. Many SQL implementations (e.g. MySQL) + * are able to cache subqueries. By keeping the ORDER BY clause intact, the limitSubQuery + * that will most likely be executed next can be read from the native SQL cache. + * + * @psalm-import-type QueryComponent from Parser + */ +class CountOutputWalker extends SqlWalker +{ + private readonly AbstractPlatform $platform; + private readonly ResultSetMapping $rsm; + + /** + * {@inheritDoc} + */ + public function __construct(Query $query, ParserResult $parserResult, array $queryComponents) + { + $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform(); + $this->rsm = $parserResult->getResultSetMapping(); + + parent::__construct($query, $parserResult, $queryComponents); + } + + public function walkSelectStatement(SelectStatement $selectStatement): string + { + if ($this->platform instanceof SQLServerPlatform) { + $selectStatement->orderByClause = null; + } + + $sql = parent::walkSelectStatement($selectStatement); + + if ($selectStatement->groupByClause) { + return sprintf( + 'SELECT COUNT(*) AS dctrn_count FROM (%s) dctrn_table', + $sql, + ); + } + + // Find out the SQL alias of the identifier column of the root entity + // It may be possible to make this work with multiple root entities but that + // would probably require issuing multiple queries or doing a UNION SELECT + // so for now, It's not supported. + + // Get the root entity and alias from the AST fromClause + $from = $selectStatement->fromClause->identificationVariableDeclarations; + if (count($from) > 1) { + throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction'); + } + + $fromRoot = reset($from); + $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; + $rootClass = $this->getMetadataForDqlAlias($rootAlias); + $rootIdentifier = $rootClass->identifier; + + // For every identifier, find out the SQL alias by combing through the ResultSetMapping + $sqlIdentifier = []; + foreach ($rootIdentifier as $property) { + if (isset($rootClass->fieldMappings[$property])) { + foreach (array_keys($this->rsm->fieldMappings, $property, true) as $alias) { + if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) { + $sqlIdentifier[$property] = $alias; + } + } + } + + if (isset($rootClass->associationMappings[$property])) { + $association = $rootClass->associationMappings[$property]; + assert($association->isToOneOwningSide()); + $joinColumn = $association->joinColumns[0]->name; + + foreach (array_keys($this->rsm->metaMappings, $joinColumn, true) as $alias) { + if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) { + $sqlIdentifier[$property] = $alias; + } + } + } + } + + if (count($rootIdentifier) !== count($sqlIdentifier)) { + throw new RuntimeException(sprintf( + 'Not all identifier properties can be found in the ResultSetMapping: %s', + implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier))), + )); + } + + // Build the counter query + return sprintf( + 'SELECT COUNT(*) AS dctrn_count FROM (SELECT DISTINCT %s FROM (%s) dctrn_result) dctrn_table', + implode(', ', $sqlIdentifier), + $sql, + ); + } +} diff --git a/vendor/doctrine/orm/src/Tools/Pagination/CountWalker.php b/vendor/doctrine/orm/src/Tools/Pagination/CountWalker.php new file mode 100644 index 0000000..d212943 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Pagination/CountWalker.php @@ -0,0 +1,68 @@ +havingClause) { + throw new RuntimeException('Cannot count query that uses a HAVING clause. Use the output walkers for pagination'); + } + + // Get the root entity and alias from the AST fromClause + $from = $selectStatement->fromClause->identificationVariableDeclarations; + + if (count($from) > 1) { + throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction'); + } + + $fromRoot = reset($from); + $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; + $rootClass = $this->getMetadataForDqlAlias($rootAlias); + $identifierFieldName = $rootClass->getSingleIdentifierFieldName(); + + $pathType = PathExpression::TYPE_STATE_FIELD; + if (isset($rootClass->associationMappings[$identifierFieldName])) { + $pathType = PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; + } + + $pathExpression = new PathExpression( + PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, + $rootAlias, + $identifierFieldName, + ); + $pathExpression->type = $pathType; + + $distinct = $this->_getQuery()->getHint(self::HINT_DISTINCT); + $selectStatement->selectClause->selectExpressions = [ + new SelectExpression( + new AggregateExpression('count', $pathExpression, $distinct), + null, + ), + ]; + + // ORDER BY is not needed, only increases query execution through unnecessary sorting. + $selectStatement->orderByClause = null; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Pagination/Exception/RowNumberOverFunctionNotEnabled.php b/vendor/doctrine/orm/src/Tools/Pagination/Exception/RowNumberOverFunctionNotEnabled.php new file mode 100644 index 0000000..0e3da93 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Pagination/Exception/RowNumberOverFunctionNotEnabled.php @@ -0,0 +1,16 @@ + FROM () LIMIT x OFFSET y + * + * Works with composite keys but cannot deal with queries that have multiple + * root entities (e.g. `SELECT f, b from Foo, Bar`) + * + * @psalm-import-type QueryComponent from Parser + */ +class LimitSubqueryOutputWalker extends SqlWalker +{ + private const ORDER_BY_PATH_EXPRESSION = '/(? */ + private array $orderByPathExpressions = []; + + /** + * We don't want to add path expressions from sub-selects into the select clause of the containing query. + * This state flag simply keeps track on whether we are walking on a subquery or not + */ + private bool $inSubSelect = false; + + /** + * Stores various parameters that are otherwise unavailable + * because Doctrine\ORM\Query\SqlWalker keeps everything private without + * accessors. + * + * {@inheritDoc} + */ + public function __construct( + Query $query, + ParserResult $parserResult, + array $queryComponents, + ) { + $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform(); + $this->rsm = $parserResult->getResultSetMapping(); + + // Reset limit and offset + $this->firstResult = $query->getFirstResult(); + $this->maxResults = $query->getMaxResults(); + $query->setFirstResult(0)->setMaxResults(null); + + $this->em = $query->getEntityManager(); + $this->quoteStrategy = $this->em->getConfiguration()->getQuoteStrategy(); + + parent::__construct($query, $parserResult, $queryComponents); + } + + /** + * Check if the platform supports the ROW_NUMBER window function. + */ + private function platformSupportsRowNumber(): bool + { + return $this->platform instanceof PostgreSQLPlatform + || $this->platform instanceof SQLServerPlatform + || $this->platform instanceof OraclePlatform + || $this->platform instanceof DB2Platform + || (method_exists($this->platform, 'supportsRowNumberFunction') + && $this->platform->supportsRowNumberFunction()); + } + + /** + * Rebuilds a select statement's order by clause for use in a + * ROW_NUMBER() OVER() expression. + */ + private function rebuildOrderByForRowNumber(SelectStatement $AST): void + { + $orderByClause = $AST->orderByClause; + $selectAliasToExpressionMap = []; + // Get any aliases that are available for select expressions. + foreach ($AST->selectClause->selectExpressions as $selectExpression) { + $selectAliasToExpressionMap[$selectExpression->fieldIdentificationVariable] = $selectExpression->expression; + } + + // Rebuild string orderby expressions to use the select expression they're referencing + foreach ($orderByClause->orderByItems as $orderByItem) { + if (is_string($orderByItem->expression) && isset($selectAliasToExpressionMap[$orderByItem->expression])) { + $orderByItem->expression = $selectAliasToExpressionMap[$orderByItem->expression]; + } + } + + $func = new RowNumberOverFunction('dctrn_rownum'); + $func->orderByClause = $AST->orderByClause; + $AST->selectClause->selectExpressions[] = new SelectExpression($func, 'dctrn_rownum', true); + + // No need for an order by clause, we'll order by rownum in the outer query. + $AST->orderByClause = null; + } + + public function walkSelectStatement(SelectStatement $selectStatement): string + { + if ($this->platformSupportsRowNumber()) { + return $this->walkSelectStatementWithRowNumber($selectStatement); + } + + return $this->walkSelectStatementWithoutRowNumber($selectStatement); + } + + /** + * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT. + * This method is for use with platforms which support ROW_NUMBER. + * + * @throws RuntimeException + */ + public function walkSelectStatementWithRowNumber(SelectStatement $AST): string + { + $hasOrderBy = false; + $outerOrderBy = ' ORDER BY dctrn_minrownum ASC'; + $orderGroupBy = ''; + if ($AST->orderByClause instanceof OrderByClause) { + $hasOrderBy = true; + $this->rebuildOrderByForRowNumber($AST); + } + + $innerSql = $this->getInnerSQL($AST); + + $sqlIdentifier = $this->getSQLIdentifier($AST); + + if ($hasOrderBy) { + $orderGroupBy = ' GROUP BY ' . implode(', ', $sqlIdentifier); + $sqlIdentifier[] = 'MIN(' . $this->walkResultVariable('dctrn_rownum') . ') AS dctrn_minrownum'; + } + + // Build the counter query + $sql = sprintf( + 'SELECT DISTINCT %s FROM (%s) dctrn_result', + implode(', ', $sqlIdentifier), + $innerSql, + ); + + if ($hasOrderBy) { + $sql .= $orderGroupBy . $outerOrderBy; + } + + // Apply the limit and offset. + $sql = $this->platform->modifyLimitQuery( + $sql, + $this->maxResults, + $this->firstResult, + ); + + // Add the columns to the ResultSetMapping. It's not really nice but + // it works. Preferably I'd clear the RSM or simply create a new one + // but that is not possible from inside the output walker, so we dirty + // up the one we have. + foreach ($sqlIdentifier as $property => $alias) { + $this->rsm->addScalarResult($alias, $property); + } + + return $sql; + } + + /** + * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT. + * This method is for platforms which DO NOT support ROW_NUMBER. + * + * @throws RuntimeException + */ + public function walkSelectStatementWithoutRowNumber(SelectStatement $AST, bool $addMissingItemsFromOrderByToSelect = true): string + { + // We don't want to call this recursively! + if ($AST->orderByClause instanceof OrderByClause && $addMissingItemsFromOrderByToSelect) { + // In the case of ordering a query by columns from joined tables, we + // must add those columns to the select clause of the query BEFORE + // the SQL is generated. + $this->addMissingItemsFromOrderByToSelect($AST); + } + + // Remove order by clause from the inner query + // It will be re-appended in the outer select generated by this method + $orderByClause = $AST->orderByClause; + $AST->orderByClause = null; + + $innerSql = $this->getInnerSQL($AST); + + $sqlIdentifier = $this->getSQLIdentifier($AST); + + // Build the counter query + $sql = sprintf( + 'SELECT DISTINCT %s FROM (%s) dctrn_result', + implode(', ', $sqlIdentifier), + $innerSql, + ); + + // https://github.com/doctrine/orm/issues/2630 + $sql = $this->preserveSqlOrdering($sqlIdentifier, $innerSql, $sql, $orderByClause); + + // Apply the limit and offset. + $sql = $this->platform->modifyLimitQuery( + $sql, + $this->maxResults, + $this->firstResult, + ); + + // Add the columns to the ResultSetMapping. It's not really nice but + // it works. Preferably I'd clear the RSM or simply create a new one + // but that is not possible from inside the output walker, so we dirty + // up the one we have. + foreach ($sqlIdentifier as $property => $alias) { + $this->rsm->addScalarResult($alias, $property); + } + + // Restore orderByClause + $AST->orderByClause = $orderByClause; + + return $sql; + } + + /** + * Finds all PathExpressions in an AST's OrderByClause, and ensures that + * the referenced fields are present in the SelectClause of the passed AST. + */ + private function addMissingItemsFromOrderByToSelect(SelectStatement $AST): void + { + $this->orderByPathExpressions = []; + + // We need to do this in another walker because otherwise we'll end up + // polluting the state of this one. + $walker = clone $this; + + // This will populate $orderByPathExpressions via + // LimitSubqueryOutputWalker::walkPathExpression, which will be called + // as the select statement is walked. We'll end up with an array of all + // path expressions referenced in the query. + $walker->walkSelectStatementWithoutRowNumber($AST, false); + $orderByPathExpressions = $walker->getOrderByPathExpressions(); + + // Get a map of referenced identifiers to field names. + $selects = []; + foreach ($orderByPathExpressions as $pathExpression) { + assert($pathExpression->field !== null); + $idVar = $pathExpression->identificationVariable; + $field = $pathExpression->field; + if (! isset($selects[$idVar])) { + $selects[$idVar] = []; + } + + $selects[$idVar][$field] = true; + } + + // Loop the select clause of the AST and exclude items from $select + // that are already being selected in the query. + foreach ($AST->selectClause->selectExpressions as $selectExpression) { + if ($selectExpression instanceof SelectExpression) { + $idVar = $selectExpression->expression; + if (! is_string($idVar)) { + continue; + } + + $field = $selectExpression->fieldIdentificationVariable; + if ($field === null) { + // No need to add this select, as we're already fetching the whole object. + unset($selects[$idVar]); + } else { + unset($selects[$idVar][$field]); + } + } + } + + // Add select items which were not excluded to the AST's select clause. + foreach ($selects as $idVar => $fields) { + $AST->selectClause->selectExpressions[] = new SelectExpression($idVar, null, true); + } + } + + /** + * Generates new SQL for statements with an order by clause + * + * @param mixed[] $sqlIdentifier + */ + private function preserveSqlOrdering( + array $sqlIdentifier, + string $innerSql, + string $sql, + OrderByClause|null $orderByClause, + ): string { + // If the sql statement has an order by clause, we need to wrap it in a new select distinct statement + if (! $orderByClause) { + return $sql; + } + + // now only select distinct identifier + return sprintf( + 'SELECT DISTINCT %s FROM (%s) dctrn_result', + implode(', ', $sqlIdentifier), + $this->recreateInnerSql($orderByClause, $sqlIdentifier, $innerSql), + ); + } + + /** + * Generates a new SQL statement for the inner query to keep the correct sorting + * + * @param mixed[] $identifiers + */ + private function recreateInnerSql( + OrderByClause $orderByClause, + array $identifiers, + string $innerSql, + ): string { + [$searchPatterns, $replacements] = $this->generateSqlAliasReplacements(); + $orderByItems = []; + + foreach ($orderByClause->orderByItems as $orderByItem) { + // Walk order by item to get string representation of it and + // replace path expressions in the order by clause with their column alias + $orderByItemString = preg_replace( + $searchPatterns, + $replacements, + $this->walkOrderByItem($orderByItem), + ); + + $orderByItems[] = $orderByItemString; + $identifier = substr($orderByItemString, 0, strrpos($orderByItemString, ' ')); + + if (! in_array($identifier, $identifiers, true)) { + $identifiers[] = $identifier; + } + } + + return $sql = sprintf( + 'SELECT DISTINCT %s FROM (%s) dctrn_result_inner ORDER BY %s', + implode(', ', $identifiers), + $innerSql, + implode(', ', $orderByItems), + ); + } + + /** + * @return string[][] + * @psalm-return array{0: list, 1: list} + */ + private function generateSqlAliasReplacements(): array + { + $aliasMap = $searchPatterns = $replacements = $metadataList = []; + + // Generate DQL alias -> SQL table alias mapping + foreach (array_keys($this->rsm->aliasMap) as $dqlAlias) { + $metadataList[$dqlAlias] = $class = $this->getMetadataForDqlAlias($dqlAlias); + $aliasMap[$dqlAlias] = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); + } + + // Generate search patterns for each field's path expression in the order by clause + foreach ($this->rsm->fieldMappings as $fieldAlias => $fieldName) { + $dqlAliasForFieldAlias = $this->rsm->columnOwnerMap[$fieldAlias]; + $class = $metadataList[$dqlAliasForFieldAlias]; + + // If the field is from a joined child table, we won't be ordering on it. + if (! isset($class->fieldMappings[$fieldName])) { + continue; + } + + $fieldMapping = $class->fieldMappings[$fieldName]; + + // Get the proper column name as will appear in the select list + $columnName = $this->quoteStrategy->getColumnName( + $fieldName, + $metadataList[$dqlAliasForFieldAlias], + $this->em->getConnection()->getDatabasePlatform(), + ); + + // Get the SQL table alias for the entity and field + $sqlTableAliasForFieldAlias = $aliasMap[$dqlAliasForFieldAlias]; + + if (isset($fieldMapping->declared) && $fieldMapping->declared !== $class->name) { + // Field was declared in a parent class, so we need to get the proper SQL table alias + // for the joined parent table. + $otherClassMetadata = $this->em->getClassMetadata($fieldMapping->declared); + + if (! $otherClassMetadata->isMappedSuperclass) { + $sqlTableAliasForFieldAlias = $this->getSQLTableAlias($otherClassMetadata->getTableName(), $dqlAliasForFieldAlias); + } + } + + // Compose search and replace patterns + $searchPatterns[] = sprintf(self::ORDER_BY_PATH_EXPRESSION, $sqlTableAliasForFieldAlias, $columnName); + $replacements[] = $fieldAlias; + } + + return [$searchPatterns, $replacements]; + } + + /** + * getter for $orderByPathExpressions + * + * @return list + */ + public function getOrderByPathExpressions(): array + { + return $this->orderByPathExpressions; + } + + /** + * @throws OptimisticLockException + * @throws QueryException + */ + private function getInnerSQL(SelectStatement $AST): string + { + // Set every select expression as visible(hidden = false) to + // make $AST have scalar mappings properly - this is relevant for referencing selected + // fields from outside the subquery, for example in the ORDER BY segment + $hiddens = []; + + foreach ($AST->selectClause->selectExpressions as $idx => $expr) { + $hiddens[$idx] = $expr->hiddenAliasResultVariable; + $expr->hiddenAliasResultVariable = false; + } + + $innerSql = parent::walkSelectStatement($AST); + + // Restore hiddens + foreach ($AST->selectClause->selectExpressions as $idx => $expr) { + $expr->hiddenAliasResultVariable = $hiddens[$idx]; + } + + return $innerSql; + } + + /** @return string[] */ + private function getSQLIdentifier(SelectStatement $AST): array + { + // Find out the SQL alias of the identifier column of the root entity. + // It may be possible to make this work with multiple root entities but that + // would probably require issuing multiple queries or doing a UNION SELECT. + // So for now, it's not supported. + + // Get the root entity and alias from the AST fromClause. + $from = $AST->fromClause->identificationVariableDeclarations; + if (count($from) !== 1) { + throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction'); + } + + $fromRoot = reset($from); + $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; + $rootClass = $this->getMetadataForDqlAlias($rootAlias); + $rootIdentifier = $rootClass->identifier; + + // For every identifier, find out the SQL alias by combing through the ResultSetMapping + $sqlIdentifier = []; + foreach ($rootIdentifier as $property) { + if (isset($rootClass->fieldMappings[$property])) { + foreach (array_keys($this->rsm->fieldMappings, $property, true) as $alias) { + if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) { + $sqlIdentifier[$property] = $alias; + } + } + } + + if (isset($rootClass->associationMappings[$property])) { + $association = $rootClass->associationMappings[$property]; + assert($association->isToOneOwningSide()); + $joinColumn = $association->joinColumns[0]->name; + + foreach (array_keys($this->rsm->metaMappings, $joinColumn, true) as $alias) { + if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) { + $sqlIdentifier[$property] = $alias; + } + } + } + } + + if (count($sqlIdentifier) === 0) { + throw new RuntimeException('The Paginator does not support Queries which only yield ScalarResults.'); + } + + if (count($rootIdentifier) !== count($sqlIdentifier)) { + throw new RuntimeException(sprintf( + 'Not all identifier properties can be found in the ResultSetMapping: %s', + implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier))), + )); + } + + return $sqlIdentifier; + } + + public function walkPathExpression(PathExpression $pathExpr): string + { + if (! $this->inSubSelect && ! $this->platformSupportsRowNumber() && ! in_array($pathExpr, $this->orderByPathExpressions, true)) { + $this->orderByPathExpressions[] = $pathExpr; + } + + return parent::walkPathExpression($pathExpr); + } + + public function walkSubSelect(Subselect $subselect): string + { + $this->inSubSelect = true; + + $sql = parent::walkSubselect($subselect); + + $this->inSubSelect = false; + + return $sql; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryWalker.php b/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryWalker.php new file mode 100644 index 0000000..3fb0eee --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryWalker.php @@ -0,0 +1,155 @@ +fromClause->identificationVariableDeclarations; + $fromRoot = reset($from); + $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; + $rootClass = $this->getMetadataForDqlAlias($rootAlias); + + $this->validate($selectStatement); + $identifier = $rootClass->getSingleIdentifierFieldName(); + + if (isset($rootClass->associationMappings[$identifier])) { + throw new RuntimeException('Paginating an entity with foreign key as identifier only works when using the Output Walkers. Call Paginator#setUseOutputWalkers(true) before iterating the paginator.'); + } + + $query = $this->_getQuery(); + + $query->setHint( + self::IDENTIFIER_TYPE, + Type::getType($rootClass->fieldMappings[$identifier]->type), + ); + + $query->setHint(self::FORCE_DBAL_TYPE_CONVERSION, true); + + $pathExpression = new PathExpression( + PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, + $rootAlias, + $identifier, + ); + + $pathExpression->type = PathExpression::TYPE_STATE_FIELD; + + $selectStatement->selectClause->selectExpressions = [new SelectExpression($pathExpression, '_dctrn_id')]; + $selectStatement->selectClause->isDistinct = ($query->getHints()[Paginator::HINT_ENABLE_DISTINCT] ?? true) === true; + + if (! isset($selectStatement->orderByClause)) { + return; + } + + $queryComponents = $this->getQueryComponents(); + foreach ($selectStatement->orderByClause->orderByItems as $item) { + if ($item->expression instanceof PathExpression) { + $selectStatement->selectClause->selectExpressions[] = new SelectExpression( + $this->createSelectExpressionItem($item->expression), + '_dctrn_ord' . $this->aliasCounter++, + ); + + continue; + } + + if (is_string($item->expression) && isset($queryComponents[$item->expression])) { + $qComp = $queryComponents[$item->expression]; + + if (isset($qComp['resultVariable'])) { + $selectStatement->selectClause->selectExpressions[] = new SelectExpression( + $qComp['resultVariable'], + $item->expression, + ); + } + } + } + } + + /** + * Validate the AST to ensure that this walker is able to properly manipulate it. + */ + private function validate(SelectStatement $AST): void + { + // Prevent LimitSubqueryWalker from being used with queries that include + // a limit, a fetched to-many join, and an order by condition that + // references a column from the fetch joined table. + $queryComponents = $this->getQueryComponents(); + $query = $this->_getQuery(); + $from = $AST->fromClause->identificationVariableDeclarations; + $fromRoot = reset($from); + + if ( + $query instanceof Query + && $query->getMaxResults() !== null + && $AST->orderByClause + && count($fromRoot->joins) + ) { + // Check each orderby item. + // TODO: check complex orderby items too... + foreach ($AST->orderByClause->orderByItems as $orderByItem) { + $expression = $orderByItem->expression; + if ( + $orderByItem->expression instanceof PathExpression + && isset($queryComponents[$expression->identificationVariable]) + ) { + $queryComponent = $queryComponents[$expression->identificationVariable]; + if ( + isset($queryComponent['parent']) + && isset($queryComponent['relation']) + && $queryComponent['relation']->isToMany() + ) { + throw new RuntimeException('Cannot select distinct identifiers from query with LIMIT and ORDER BY on a column from a fetch joined to-many association. Use output walkers.'); + } + } + } + } + } + + /** + * Retrieve either an IdentityFunction (IDENTITY(u.assoc)) or a state field (u.name). + * + * @return IdentityFunction|PathExpression + */ + private function createSelectExpressionItem(PathExpression $pathExpression): Node + { + if ($pathExpression->type === PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) { + $identity = new IdentityFunction('identity'); + + $identity->pathExpression = clone $pathExpression; + + return $identity; + } + + return clone $pathExpression; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Pagination/Paginator.php b/vendor/doctrine/orm/src/Tools/Pagination/Paginator.php new file mode 100644 index 0000000..db1b34d --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Pagination/Paginator.php @@ -0,0 +1,263 @@ + + */ +class Paginator implements Countable, IteratorAggregate +{ + use SQLResultCasing; + + public const HINT_ENABLE_DISTINCT = 'paginator.distinct.enable'; + + private readonly Query $query; + private bool|null $useOutputWalkers = null; + private int|null $count = null; + + /** @param bool $fetchJoinCollection Whether the query joins a collection (true by default). */ + public function __construct( + Query|QueryBuilder $query, + private readonly bool $fetchJoinCollection = true, + ) { + if ($query instanceof QueryBuilder) { + $query = $query->getQuery(); + } + + $this->query = $query; + } + + /** + * Returns the query. + */ + public function getQuery(): Query + { + return $this->query; + } + + /** + * Returns whether the query joins a collection. + * + * @return bool Whether the query joins a collection. + */ + public function getFetchJoinCollection(): bool + { + return $this->fetchJoinCollection; + } + + /** + * Returns whether the paginator will use an output walker. + */ + public function getUseOutputWalkers(): bool|null + { + return $this->useOutputWalkers; + } + + /** + * Sets whether the paginator will use an output walker. + * + * @return $this + */ + public function setUseOutputWalkers(bool|null $useOutputWalkers): static + { + $this->useOutputWalkers = $useOutputWalkers; + + return $this; + } + + public function count(): int + { + if ($this->count === null) { + try { + $this->count = (int) array_sum(array_map('current', $this->getCountQuery()->getScalarResult())); + } catch (NoResultException) { + $this->count = 0; + } + } + + return $this->count; + } + + /** + * {@inheritDoc} + * + * @psalm-return Traversable + */ + public function getIterator(): Traversable + { + $offset = $this->query->getFirstResult(); + $length = $this->query->getMaxResults(); + + if ($this->fetchJoinCollection && $length !== null) { + $subQuery = $this->cloneQuery($this->query); + + if ($this->useOutputWalker($subQuery)) { + $subQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class); + } else { + $this->appendTreeWalker($subQuery, LimitSubqueryWalker::class); + $this->unbindUnusedQueryParams($subQuery); + } + + $subQuery->setFirstResult($offset)->setMaxResults($length); + + $foundIdRows = $subQuery->getScalarResult(); + + // don't do this for an empty id array + if ($foundIdRows === []) { + return new ArrayIterator([]); + } + + $whereInQuery = $this->cloneQuery($this->query); + $ids = array_map('current', $foundIdRows); + + $this->appendTreeWalker($whereInQuery, WhereInWalker::class); + $whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_HAS_IDS, true); + $whereInQuery->setFirstResult(0)->setMaxResults(null); + $whereInQuery->setCacheable($this->query->isCacheable()); + + $databaseIds = $this->convertWhereInIdentifiersToDatabaseValues($ids); + $whereInQuery->setParameter(WhereInWalker::PAGINATOR_ID_ALIAS, $databaseIds); + + $result = $whereInQuery->getResult($this->query->getHydrationMode()); + } else { + $result = $this->cloneQuery($this->query) + ->setMaxResults($length) + ->setFirstResult($offset) + ->setCacheable($this->query->isCacheable()) + ->getResult($this->query->getHydrationMode()); + } + + return new ArrayIterator($result); + } + + private function cloneQuery(Query $query): Query + { + $cloneQuery = clone $query; + + $cloneQuery->setParameters(clone $query->getParameters()); + $cloneQuery->setCacheable(false); + + foreach ($query->getHints() as $name => $value) { + $cloneQuery->setHint($name, $value); + } + + return $cloneQuery; + } + + /** + * Determines whether to use an output walker for the query. + */ + private function useOutputWalker(Query $query): bool + { + if ($this->useOutputWalkers === null) { + return (bool) $query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER) === false; + } + + return $this->useOutputWalkers; + } + + /** + * Appends a custom tree walker to the tree walkers hint. + * + * @psalm-param class-string $walkerClass + */ + private function appendTreeWalker(Query $query, string $walkerClass): void + { + $hints = $query->getHint(Query::HINT_CUSTOM_TREE_WALKERS); + + if ($hints === false) { + $hints = []; + } + + $hints[] = $walkerClass; + $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, $hints); + } + + /** + * Returns Query prepared to count. + */ + private function getCountQuery(): Query + { + $countQuery = $this->cloneQuery($this->query); + + if (! $countQuery->hasHint(CountWalker::HINT_DISTINCT)) { + $countQuery->setHint(CountWalker::HINT_DISTINCT, true); + } + + if ($this->useOutputWalker($countQuery)) { + $platform = $countQuery->getEntityManager()->getConnection()->getDatabasePlatform(); // law of demeter win + + $rsm = new ResultSetMapping(); + $rsm->addScalarResult($this->getSQLResultCasing($platform, 'dctrn_count'), 'count'); + + $countQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, CountOutputWalker::class); + $countQuery->setResultSetMapping($rsm); + } else { + $this->appendTreeWalker($countQuery, CountWalker::class); + $this->unbindUnusedQueryParams($countQuery); + } + + $countQuery->setFirstResult(0)->setMaxResults(null); + + return $countQuery; + } + + private function unbindUnusedQueryParams(Query $query): void + { + $parser = new Parser($query); + $parameterMappings = $parser->parse()->getParameterMappings(); + /** @var Collection|Parameter[] $parameters */ + $parameters = $query->getParameters(); + + foreach ($parameters as $key => $parameter) { + $parameterName = $parameter->getName(); + + if (! (isset($parameterMappings[$parameterName]) || array_key_exists($parameterName, $parameterMappings))) { + unset($parameters[$key]); + } + } + + $query->setParameters($parameters); + } + + /** + * @param mixed[] $identifiers + * + * @return mixed[] + */ + private function convertWhereInIdentifiersToDatabaseValues(array $identifiers): array + { + $query = $this->cloneQuery($this->query); + $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, RootTypeWalker::class); + + $connection = $this->query->getEntityManager()->getConnection(); + $type = $query->getSQL(); + assert(is_string($type)); + + return array_map(static fn ($id): mixed => $connection->convertToDatabaseValue($id, $type), $identifiers); + } +} diff --git a/vendor/doctrine/orm/src/Tools/Pagination/RootTypeWalker.php b/vendor/doctrine/orm/src/Tools/Pagination/RootTypeWalker.php new file mode 100644 index 0000000..f630ee1 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Pagination/RootTypeWalker.php @@ -0,0 +1,48 @@ + root entity id type resolution can be cached in the query cache. + */ +final class RootTypeWalker extends SqlWalker +{ + public function walkSelectStatement(AST\SelectStatement $selectStatement): string + { + // Get the root entity and alias from the AST fromClause + $from = $selectStatement->fromClause->identificationVariableDeclarations; + + if (count($from) > 1) { + throw new RuntimeException('Can only process queries that select only one FROM component'); + } + + $fromRoot = reset($from); + $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; + $rootClass = $this->getMetadataForDqlAlias($rootAlias); + $identifierFieldName = $rootClass->getSingleIdentifierFieldName(); + + return PersisterHelper::getTypeOfField( + $identifierFieldName, + $rootClass, + $this->getQuery() + ->getEntityManager(), + )[0]; + } +} diff --git a/vendor/doctrine/orm/src/Tools/Pagination/RowNumberOverFunction.php b/vendor/doctrine/orm/src/Tools/Pagination/RowNumberOverFunction.php new file mode 100644 index 0000000..a0fdd01 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Pagination/RowNumberOverFunction.php @@ -0,0 +1,40 @@ +walkOrderByClause( + $this->orderByClause, + )) . ')'; + } + + /** + * @throws RowNumberOverFunctionNotEnabled + * + * @inheritdoc + */ + public function parse(Parser $parser): void + { + throw RowNumberOverFunctionNotEnabled::create(); + } +} diff --git a/vendor/doctrine/orm/src/Tools/Pagination/WhereInWalker.php b/vendor/doctrine/orm/src/Tools/Pagination/WhereInWalker.php new file mode 100644 index 0000000..01741ca --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Pagination/WhereInWalker.php @@ -0,0 +1,116 @@ +fromClause->identificationVariableDeclarations; + + if (count($from) > 1) { + throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction'); + } + + $fromRoot = reset($from); + $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; + $rootClass = $this->getMetadataForDqlAlias($rootAlias); + $identifierFieldName = $rootClass->getSingleIdentifierFieldName(); + + $pathType = PathExpression::TYPE_STATE_FIELD; + if (isset($rootClass->associationMappings[$identifierFieldName])) { + $pathType = PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; + } + + $pathExpression = new PathExpression(PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $rootAlias, $identifierFieldName); + $pathExpression->type = $pathType; + + $hasIds = $this->_getQuery()->getHint(self::HINT_PAGINATOR_HAS_IDS); + + if ($hasIds) { + $arithmeticExpression = new ArithmeticExpression(); + $arithmeticExpression->simpleArithmeticExpression = new SimpleArithmeticExpression( + [$pathExpression], + ); + $expression = new InListExpression( + $arithmeticExpression, + [new InputParameter(':' . self::PAGINATOR_ID_ALIAS)], + ); + } else { + $expression = new NullComparisonExpression($pathExpression); + } + + $conditionalPrimary = new ConditionalPrimary(); + $conditionalPrimary->simpleConditionalExpression = $expression; + if ($selectStatement->whereClause) { + if ($selectStatement->whereClause->conditionalExpression instanceof ConditionalTerm) { + $selectStatement->whereClause->conditionalExpression->conditionalFactors[] = $conditionalPrimary; + } elseif ($selectStatement->whereClause->conditionalExpression instanceof ConditionalPrimary) { + $selectStatement->whereClause->conditionalExpression = new ConditionalExpression( + [ + new ConditionalTerm( + [ + $selectStatement->whereClause->conditionalExpression, + $conditionalPrimary, + ], + ), + ], + ); + } else { + $tmpPrimary = new ConditionalPrimary(); + $tmpPrimary->conditionalExpression = $selectStatement->whereClause->conditionalExpression; + $selectStatement->whereClause->conditionalExpression = new ConditionalTerm( + [ + $tmpPrimary, + $conditionalPrimary, + ], + ); + } + } else { + $selectStatement->whereClause = new WhereClause( + new ConditionalExpression( + [new ConditionalTerm([$conditionalPrimary])], + ), + ); + } + } +} diff --git a/vendor/doctrine/orm/src/Tools/ResolveTargetEntityListener.php b/vendor/doctrine/orm/src/Tools/ResolveTargetEntityListener.php new file mode 100644 index 0000000..9e48521 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/ResolveTargetEntityListener.php @@ -0,0 +1,117 @@ + $mapping + */ + public function addResolveTargetEntity(string $originalEntity, string $newEntity, array $mapping): void + { + $mapping['targetEntity'] = ltrim($newEntity, '\\'); + $this->resolveTargetEntities[ltrim($originalEntity, '\\')] = $mapping; + } + + /** @internal this is an event callback, and should not be called directly */ + public function onClassMetadataNotFound(OnClassMetadataNotFoundEventArgs $args): void + { + if (array_key_exists($args->getClassName(), $this->resolveTargetEntities)) { + $args->setFoundMetadata( + $args + ->getObjectManager() + ->getClassMetadata($this->resolveTargetEntities[$args->getClassName()]['targetEntity']), + ); + } + } + + /** + * Processes event and resolves new target entity names. + * + * @internal this is an event callback, and should not be called directly + */ + public function loadClassMetadata(LoadClassMetadataEventArgs $args): void + { + $cm = $args->getClassMetadata(); + + foreach ($cm->associationMappings as $mapping) { + if (isset($this->resolveTargetEntities[$mapping->targetEntity])) { + $this->remapAssociation($cm, $mapping); + } + } + + foreach ($this->resolveTargetEntities as $interface => $data) { + if ($data['targetEntity'] === $cm->getName()) { + $args->getEntityManager()->getMetadataFactory()->setMetadataFor($interface, $cm); + } + } + + foreach ($cm->discriminatorMap as $value => $class) { + if (isset($this->resolveTargetEntities[$class])) { + $cm->addDiscriminatorMapClass($value, $this->resolveTargetEntities[$class]['targetEntity']); + } + } + } + + private function remapAssociation(ClassMetadata $classMetadata, AssociationMapping $mapping): void + { + $newMapping = $this->resolveTargetEntities[$mapping->targetEntity]; + $newMapping = array_replace_recursive( + $mapping->toArray(), + $newMapping, + ); + $newMapping['fieldName'] = $mapping->fieldName; + + unset($classMetadata->associationMappings[$mapping->fieldName]); + + switch ($mapping->type()) { + case ClassMetadata::MANY_TO_MANY: + $classMetadata->mapManyToMany($newMapping); + break; + case ClassMetadata::MANY_TO_ONE: + $classMetadata->mapManyToOne($newMapping); + break; + case ClassMetadata::ONE_TO_MANY: + $classMetadata->mapOneToMany($newMapping); + break; + case ClassMetadata::ONE_TO_ONE: + $classMetadata->mapOneToOne($newMapping); + break; + } + } +} diff --git a/vendor/doctrine/orm/src/Tools/SchemaTool.php b/vendor/doctrine/orm/src/Tools/SchemaTool.php new file mode 100644 index 0000000..42b52df --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/SchemaTool.php @@ -0,0 +1,932 @@ +ClassMetadata class descriptors. + * + * @link www.doctrine-project.org + */ +class SchemaTool +{ + private const KNOWN_COLUMN_OPTIONS = ['comment', 'unsigned', 'fixed', 'default']; + + private readonly AbstractPlatform $platform; + private readonly QuoteStrategy $quoteStrategy; + private readonly AbstractSchemaManager $schemaManager; + + /** + * Initializes a new SchemaTool instance that uses the connection of the + * provided EntityManager. + */ + public function __construct(private readonly EntityManagerInterface $em) + { + $this->platform = $em->getConnection()->getDatabasePlatform(); + $this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy(); + $this->schemaManager = $em->getConnection()->createSchemaManager(); + } + + /** + * Creates the database schema for the given array of ClassMetadata instances. + * + * @psalm-param list $classes + * + * @throws ToolsException + */ + public function createSchema(array $classes): void + { + $createSchemaSql = $this->getCreateSchemaSql($classes); + $conn = $this->em->getConnection(); + + foreach ($createSchemaSql as $sql) { + try { + $conn->executeStatement($sql); + } catch (Throwable $e) { + throw ToolsException::schemaToolFailure($sql, $e); + } + } + } + + /** + * Gets the list of DDL statements that are required to create the database schema for + * the given list of ClassMetadata instances. + * + * @psalm-param list $classes + * + * @return list The SQL statements needed to create the schema for the classes. + */ + public function getCreateSchemaSql(array $classes): array + { + $schema = $this->getSchemaFromMetadata($classes); + + return $schema->toSql($this->platform); + } + + /** + * Detects instances of ClassMetadata that don't need to be processed in the SchemaTool context. + * + * @psalm-param array $processedClasses + */ + private function processingNotRequired( + ClassMetadata $class, + array $processedClasses, + ): bool { + return isset($processedClasses[$class->name]) || + $class->isMappedSuperclass || + $class->isEmbeddedClass || + ($class->isInheritanceTypeSingleTable() && $class->name !== $class->rootEntityName) || + in_array($class->name, $this->em->getConfiguration()->getSchemaIgnoreClasses()); + } + + /** + * Resolves fields in index mapping to column names + * + * @param mixed[] $indexData index or unique constraint data + * + * @return list Column names from combined fields and columns mappings + */ + private function getIndexColumns(ClassMetadata $class, array $indexData): array + { + $columns = []; + + if ( + isset($indexData['columns'], $indexData['fields']) + || ( + ! isset($indexData['columns']) + && ! isset($indexData['fields']) + ) + ) { + throw MappingException::invalidIndexConfiguration( + (string) $class, + $indexData['name'] ?? 'unnamed', + ); + } + + if (isset($indexData['columns'])) { + $columns = $indexData['columns']; + } + + if (isset($indexData['fields'])) { + foreach ($indexData['fields'] as $fieldName) { + if ($class->hasField($fieldName)) { + $columns[] = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); + } elseif ($class->hasAssociation($fieldName)) { + $assoc = $class->getAssociationMapping($fieldName); + assert($assoc->isToOneOwningSide()); + foreach ($assoc->joinColumns as $joinColumn) { + $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); + } + } + } + } + + return $columns; + } + + /** + * Creates a Schema instance from a given set of metadata classes. + * + * @psalm-param list $classes + * + * @throws NotSupported + */ + public function getSchemaFromMetadata(array $classes): Schema + { + // Reminder for processed classes, used for hierarchies + $processedClasses = []; + $eventManager = $this->em->getEventManager(); + $metadataSchemaConfig = $this->schemaManager->createSchemaConfig(); + + $schema = new Schema([], [], $metadataSchemaConfig); + + $addedFks = []; + $blacklistedFks = []; + + foreach ($classes as $class) { + if ($this->processingNotRequired($class, $processedClasses)) { + continue; + } + + $table = $schema->createTable($this->quoteStrategy->getTableName($class, $this->platform)); + + if ($class->isInheritanceTypeSingleTable()) { + $this->gatherColumns($class, $table); + $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks); + + // Add the discriminator column + $this->addDiscriminatorColumnDefinition($class, $table); + + // Aggregate all the information from all classes in the hierarchy + foreach ($class->parentClasses as $parentClassName) { + // Parent class information is already contained in this class + $processedClasses[$parentClassName] = true; + } + + foreach ($class->subClasses as $subClassName) { + $subClass = $this->em->getClassMetadata($subClassName); + $this->gatherColumns($subClass, $table); + $this->gatherRelationsSql($subClass, $table, $schema, $addedFks, $blacklistedFks); + $processedClasses[$subClassName] = true; + } + } elseif ($class->isInheritanceTypeJoined()) { + // Add all non-inherited fields as columns + foreach ($class->fieldMappings as $fieldName => $mapping) { + if (! isset($mapping->inherited)) { + $this->gatherColumn($class, $mapping, $table); + } + } + + $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks); + + // Add the discriminator column only to the root table + if ($class->name === $class->rootEntityName) { + $this->addDiscriminatorColumnDefinition($class, $table); + } else { + // Add an ID FK column to child tables + $pkColumns = []; + $inheritedKeyColumns = []; + + foreach ($class->identifier as $identifierField) { + if (isset($class->fieldMappings[$identifierField]->inherited)) { + $idMapping = $class->fieldMappings[$identifierField]; + $this->gatherColumn($class, $idMapping, $table); + $columnName = $this->quoteStrategy->getColumnName( + $identifierField, + $class, + $this->platform, + ); + // TODO: This seems rather hackish, can we optimize it? + $table->getColumn($columnName)->setAutoincrement(false); + + $pkColumns[] = $columnName; + $inheritedKeyColumns[] = $columnName; + + continue; + } + + if (isset($class->associationMappings[$identifierField]->inherited)) { + $idMapping = $class->associationMappings[$identifierField]; + assert($idMapping->isToOneOwningSide()); + + $targetEntity = current( + array_filter( + $classes, + static fn (ClassMetadata $class): bool => $class->name === $idMapping->targetEntity, + ), + ); + + foreach ($idMapping->joinColumns as $joinColumn) { + if (isset($targetEntity->fieldMappings[$joinColumn->referencedColumnName])) { + $columnName = $this->quoteStrategy->getJoinColumnName( + $joinColumn, + $class, + $this->platform, + ); + + $pkColumns[] = $columnName; + $inheritedKeyColumns[] = $columnName; + } + } + } + } + + if ($inheritedKeyColumns !== []) { + // Add a FK constraint on the ID column + $table->addForeignKeyConstraint( + $this->quoteStrategy->getTableName( + $this->em->getClassMetadata($class->rootEntityName), + $this->platform, + ), + $inheritedKeyColumns, + $inheritedKeyColumns, + ['onDelete' => 'CASCADE'], + ); + } + + if ($pkColumns !== []) { + $table->setPrimaryKey($pkColumns); + } + } + } else { + $this->gatherColumns($class, $table); + $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks); + } + + $pkColumns = []; + + foreach ($class->identifier as $identifierField) { + if (isset($class->fieldMappings[$identifierField])) { + $pkColumns[] = $this->quoteStrategy->getColumnName($identifierField, $class, $this->platform); + } elseif (isset($class->associationMappings[$identifierField])) { + $assoc = $class->associationMappings[$identifierField]; + assert($assoc->isToOneOwningSide()); + + foreach ($assoc->joinColumns as $joinColumn) { + $pkColumns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); + } + } + } + + if (! $table->hasIndex('primary')) { + $table->setPrimaryKey($pkColumns); + } + + // there can be unique indexes automatically created for join column + // if join column is also primary key we should keep only primary key on this column + // so, remove indexes overruled by primary key + $primaryKey = $table->getIndex('primary'); + + foreach ($table->getIndexes() as $idxKey => $existingIndex) { + if ($primaryKey->overrules($existingIndex)) { + $table->dropIndex($idxKey); + } + } + + if (isset($class->table['indexes'])) { + foreach ($class->table['indexes'] as $indexName => $indexData) { + if (! isset($indexData['flags'])) { + $indexData['flags'] = []; + } + + $table->addIndex( + $this->getIndexColumns($class, $indexData), + is_numeric($indexName) ? null : $indexName, + (array) $indexData['flags'], + $indexData['options'] ?? [], + ); + } + } + + if (isset($class->table['uniqueConstraints'])) { + foreach ($class->table['uniqueConstraints'] as $indexName => $indexData) { + $uniqIndex = new Index('tmp__' . $indexName, $this->getIndexColumns($class, $indexData), true, false, [], $indexData['options'] ?? []); + + foreach ($table->getIndexes() as $tableIndexName => $tableIndex) { + if ($tableIndex->isFulfilledBy($uniqIndex)) { + $table->dropIndex($tableIndexName); + break; + } + } + + $table->addUniqueIndex($uniqIndex->getColumns(), is_numeric($indexName) ? null : $indexName, $indexData['options'] ?? []); + } + } + + if (isset($class->table['options'])) { + foreach ($class->table['options'] as $key => $val) { + $table->addOption($key, $val); + } + } + + $processedClasses[$class->name] = true; + + if ($class->isIdGeneratorSequence() && $class->name === $class->rootEntityName) { + $seqDef = $class->sequenceGeneratorDefinition; + $quotedName = $this->quoteStrategy->getSequenceName($seqDef, $class, $this->platform); + if (! $schema->hasSequence($quotedName)) { + $schema->createSequence( + $quotedName, + (int) $seqDef['allocationSize'], + (int) $seqDef['initialValue'], + ); + } + } + + if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) { + $eventManager->dispatchEvent( + ToolEvents::postGenerateSchemaTable, + new GenerateSchemaTableEventArgs($class, $schema, $table), + ); + } + } + + if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) { + $eventManager->dispatchEvent( + ToolEvents::postGenerateSchema, + new GenerateSchemaEventArgs($this->em, $schema), + ); + } + + return $schema; + } + + /** + * Gets a portable column definition as required by the DBAL for the discriminator + * column of a class. + */ + private function addDiscriminatorColumnDefinition(ClassMetadata $class, Table $table): void + { + $discrColumn = $class->discriminatorColumn; + assert($discrColumn !== null); + + if (strtolower($discrColumn->type) === 'string' && ! isset($discrColumn->length)) { + $discrColumn->type = 'string'; + $discrColumn->length = 255; + } + + $options = [ + 'length' => $discrColumn->length ?? null, + 'notnull' => true, + ]; + + if (isset($discrColumn->columnDefinition)) { + $options['columnDefinition'] = $discrColumn->columnDefinition; + } + + $options = $this->gatherColumnOptions($discrColumn) + $options; + $table->addColumn($discrColumn->name, $discrColumn->type, $options); + } + + /** + * Gathers the column definitions as required by the DBAL of all field mappings + * found in the given class. + */ + private function gatherColumns(ClassMetadata $class, Table $table): void + { + $pkColumns = []; + + foreach ($class->fieldMappings as $mapping) { + if ($class->isInheritanceTypeSingleTable() && isset($mapping->inherited)) { + continue; + } + + $this->gatherColumn($class, $mapping, $table); + + if ($class->isIdentifier($mapping->fieldName)) { + $pkColumns[] = $this->quoteStrategy->getColumnName($mapping->fieldName, $class, $this->platform); + } + } + } + + /** + * Creates a column definition as required by the DBAL from an ORM field mapping definition. + * + * @param ClassMetadata $class The class that owns the field mapping. + * @psalm-param FieldMapping $mapping The field mapping. + */ + private function gatherColumn( + ClassMetadata $class, + FieldMapping $mapping, + Table $table, + ): void { + $columnName = $this->quoteStrategy->getColumnName($mapping->fieldName, $class, $this->platform); + $columnType = $mapping->type; + + $options = []; + $options['length'] = $mapping->length ?? null; + $options['notnull'] = isset($mapping->nullable) ? ! $mapping->nullable : true; + if ($class->isInheritanceTypeSingleTable() && $class->parentClasses) { + $options['notnull'] = false; + } + + $options['platformOptions'] = []; + $options['platformOptions']['version'] = $class->isVersioned && $class->versionField === $mapping->fieldName; + + if (strtolower($columnType) === 'string' && $options['length'] === null) { + $options['length'] = 255; + } + + if (isset($mapping->precision)) { + $options['precision'] = $mapping->precision; + } + + if (isset($mapping->scale)) { + $options['scale'] = $mapping->scale; + } + + if (isset($mapping->default)) { + $options['default'] = $mapping->default; + } + + if (isset($mapping->columnDefinition)) { + $options['columnDefinition'] = $mapping->columnDefinition; + } + + // the 'default' option can be overwritten here + $options = $this->gatherColumnOptions($mapping) + $options; + + if ($class->isIdGeneratorIdentity() && $class->getIdentifierFieldNames() === [$mapping->fieldName]) { + $options['autoincrement'] = true; + } + + if ($class->isInheritanceTypeJoined() && $class->name !== $class->rootEntityName) { + $options['autoincrement'] = false; + } + + if ($table->hasColumn($columnName)) { + // required in some inheritance scenarios + $table->modifyColumn($columnName, $options); + } else { + $table->addColumn($columnName, $columnType, $options); + } + + $isUnique = $mapping->unique ?? false; + if ($isUnique) { + $table->addUniqueIndex([$columnName]); + } + } + + /** + * Gathers the SQL for properly setting up the relations of the given class. + * This includes the SQL for foreign key constraints and join tables. + * + * @psalm-param array + * }> $addedFks + * @psalm-param array $blacklistedFks + * + * @throws NotSupported + */ + private function gatherRelationsSql( + ClassMetadata $class, + Table $table, + Schema $schema, + array &$addedFks, + array &$blacklistedFks, + ): void { + foreach ($class->associationMappings as $id => $mapping) { + if (isset($mapping->inherited) && ! in_array($id, $class->identifier, true)) { + continue; + } + + $foreignClass = $this->em->getClassMetadata($mapping->targetEntity); + + if ($mapping->isToOneOwningSide()) { + $primaryKeyColumns = []; // PK is unnecessary for this relation-type + + $this->gatherRelationJoinColumns( + $mapping->joinColumns, + $table, + $foreignClass, + $mapping, + $primaryKeyColumns, + $addedFks, + $blacklistedFks, + ); + } elseif ($mapping instanceof ManyToManyOwningSideMapping) { + // create join table + $joinTable = $mapping->joinTable; + + $theJoinTable = $schema->createTable( + $this->quoteStrategy->getJoinTableName($mapping, $foreignClass, $this->platform), + ); + + foreach ($joinTable->options as $key => $val) { + $theJoinTable->addOption($key, $val); + } + + $primaryKeyColumns = []; + + // Build first FK constraint (relation table => source table) + $this->gatherRelationJoinColumns( + $joinTable->joinColumns, + $theJoinTable, + $class, + $mapping, + $primaryKeyColumns, + $addedFks, + $blacklistedFks, + ); + + // Build second FK constraint (relation table => target table) + $this->gatherRelationJoinColumns( + $joinTable->inverseJoinColumns, + $theJoinTable, + $foreignClass, + $mapping, + $primaryKeyColumns, + $addedFks, + $blacklistedFks, + ); + + $theJoinTable->setPrimaryKey($primaryKeyColumns); + } + } + } + + /** + * Gets the class metadata that is responsible for the definition of the referenced column name. + * + * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its + * not a simple field, go through all identifier field names that are associations recursively and + * find that referenced column name. + * + * TODO: Is there any way to make this code more pleasing? + * + * @psalm-return array{ClassMetadata, string}|null + */ + private function getDefiningClass(ClassMetadata $class, string $referencedColumnName): array|null + { + $referencedFieldName = $class->getFieldName($referencedColumnName); + + if ($class->hasField($referencedFieldName)) { + return [$class, $referencedFieldName]; + } + + if (in_array($referencedColumnName, $class->getIdentifierColumnNames(), true)) { + // it seems to be an entity as foreign key + foreach ($class->getIdentifierFieldNames() as $fieldName) { + if ( + $class->hasAssociation($fieldName) + && $class->getSingleAssociationJoinColumnName($fieldName) === $referencedColumnName + ) { + return $this->getDefiningClass( + $this->em->getClassMetadata($class->associationMappings[$fieldName]->targetEntity), + $class->getSingleAssociationReferencedJoinColumnName($fieldName), + ); + } + } + } + + return null; + } + + /** + * Gathers columns and fk constraints that are required for one part of relationship. + * + * @psalm-param list $joinColumns + * @psalm-param list $primaryKeyColumns + * @psalm-param array + * }> $addedFks + * @psalm-param array $blacklistedFks + * + * @throws MissingColumnException + */ + private function gatherRelationJoinColumns( + array $joinColumns, + Table $theJoinTable, + ClassMetadata $class, + AssociationMapping $mapping, + array &$primaryKeyColumns, + array &$addedFks, + array &$blacklistedFks, + ): void { + $localColumns = []; + $foreignColumns = []; + $fkOptions = []; + $foreignTableName = $this->quoteStrategy->getTableName($class, $this->platform); + $uniqueConstraints = []; + + foreach ($joinColumns as $joinColumn) { + [$definingClass, $referencedFieldName] = $this->getDefiningClass( + $class, + $joinColumn->referencedColumnName, + ); + + if (! $definingClass) { + throw MissingColumnException::fromColumnSourceAndTarget( + $joinColumn->referencedColumnName, + $mapping->sourceEntity, + $mapping->targetEntity, + ); + } + + $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); + $quotedRefColumnName = $this->quoteStrategy->getReferencedJoinColumnName( + $joinColumn, + $class, + $this->platform, + ); + + $primaryKeyColumns[] = $quotedColumnName; + $localColumns[] = $quotedColumnName; + $foreignColumns[] = $quotedRefColumnName; + + if (! $theJoinTable->hasColumn($quotedColumnName)) { + // Only add the column to the table if it does not exist already. + // It might exist already if the foreign key is mapped into a regular + // property as well. + + $fieldMapping = $definingClass->getFieldMapping($referencedFieldName); + + $columnOptions = ['notnull' => false]; + + if (isset($joinColumn->columnDefinition)) { + $columnOptions['columnDefinition'] = $joinColumn->columnDefinition; + } elseif (isset($fieldMapping->columnDefinition)) { + $columnOptions['columnDefinition'] = $fieldMapping->columnDefinition; + } + + if (isset($joinColumn->nullable)) { + $columnOptions['notnull'] = ! $joinColumn->nullable; + } + + $columnOptions += $this->gatherColumnOptions($fieldMapping); + + if (isset($fieldMapping->length)) { + $columnOptions['length'] = $fieldMapping->length; + } + + if ($fieldMapping->type === 'decimal') { + $columnOptions['scale'] = $fieldMapping->scale; + $columnOptions['precision'] = $fieldMapping->precision; + } + + $columnOptions = $this->gatherColumnOptions($joinColumn) + $columnOptions; + + $theJoinTable->addColumn($quotedColumnName, $fieldMapping->type, $columnOptions); + } + + if (isset($joinColumn->unique) && $joinColumn->unique === true) { + $uniqueConstraints[] = ['columns' => [$quotedColumnName]]; + } + + if (isset($joinColumn->onDelete)) { + $fkOptions['onDelete'] = $joinColumn->onDelete; + } + } + + // Prefer unique constraints over implicit simple indexes created for foreign keys. + // Also avoids index duplication. + foreach ($uniqueConstraints as $indexName => $unique) { + $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName); + } + + $compositeName = $theJoinTable->getName() . '.' . implode('', $localColumns); + if ( + isset($addedFks[$compositeName]) + && ($foreignTableName !== $addedFks[$compositeName]['foreignTableName'] + || 0 < count(array_diff($foreignColumns, $addedFks[$compositeName]['foreignColumns']))) + ) { + foreach ($theJoinTable->getForeignKeys() as $fkName => $key) { + if ( + count(array_diff($key->getLocalColumns(), $localColumns)) === 0 + && (($key->getForeignTableName() !== $foreignTableName) + || 0 < count(array_diff($key->getForeignColumns(), $foreignColumns))) + ) { + $theJoinTable->removeForeignKey($fkName); + break; + } + } + + $blacklistedFks[$compositeName] = true; + } elseif (! isset($blacklistedFks[$compositeName])) { + $addedFks[$compositeName] = ['foreignTableName' => $foreignTableName, 'foreignColumns' => $foreignColumns]; + $theJoinTable->addForeignKeyConstraint( + $foreignTableName, + $localColumns, + $foreignColumns, + $fkOptions, + ); + } + } + + /** @return mixed[] */ + private function gatherColumnOptions(JoinColumnMapping|FieldMapping|DiscriminatorColumnMapping $mapping): array + { + $mappingOptions = $mapping->options ?? []; + + if (isset($mapping->enumType)) { + $mappingOptions['enumType'] = $mapping->enumType; + } + + if (($mappingOptions['default'] ?? null) instanceof BackedEnum) { + $mappingOptions['default'] = $mappingOptions['default']->value; + } + + if (empty($mappingOptions)) { + return []; + } + + $options = array_intersect_key($mappingOptions, array_flip(self::KNOWN_COLUMN_OPTIONS)); + $options['platformOptions'] = array_diff_key($mappingOptions, $options); + + return $options; + } + + /** + * Drops the database schema for the given classes. + * + * In any way when an exception is thrown it is suppressed since drop was + * issued for all classes of the schema and some probably just don't exist. + * + * @psalm-param list $classes + */ + public function dropSchema(array $classes): void + { + $dropSchemaSql = $this->getDropSchemaSQL($classes); + $conn = $this->em->getConnection(); + + foreach ($dropSchemaSql as $sql) { + try { + $conn->executeStatement($sql); + } catch (Throwable) { + // ignored + } + } + } + + /** + * Drops all elements in the database of the current connection. + */ + public function dropDatabase(): void + { + $dropSchemaSql = $this->getDropDatabaseSQL(); + $conn = $this->em->getConnection(); + + foreach ($dropSchemaSql as $sql) { + $conn->executeStatement($sql); + } + } + + /** + * Gets the SQL needed to drop the database schema for the connections database. + * + * @return list + */ + public function getDropDatabaseSQL(): array + { + return $this->schemaManager + ->introspectSchema() + ->toDropSql($this->platform); + } + + /** + * Gets SQL to drop the tables defined by the passed classes. + * + * @psalm-param list $classes + * + * @return list + */ + public function getDropSchemaSQL(array $classes): array + { + $schema = $this->getSchemaFromMetadata($classes); + + $deployedSchema = $this->schemaManager->introspectSchema(); + + foreach ($schema->getTables() as $table) { + if (! $deployedSchema->hasTable($table->getName())) { + $schema->dropTable($table->getName()); + } + } + + if ($this->platform->supportsSequences()) { + foreach ($schema->getSequences() as $sequence) { + if (! $deployedSchema->hasSequence($sequence->getName())) { + $schema->dropSequence($sequence->getName()); + } + } + + foreach ($schema->getTables() as $table) { + $primaryKey = $table->getPrimaryKey(); + if ($primaryKey === null) { + continue; + } + + $columns = $primaryKey->getColumns(); + if (count($columns) === 1) { + $checkSequence = $table->getName() . '_' . $columns[0] . '_seq'; + if ($deployedSchema->hasSequence($checkSequence) && ! $schema->hasSequence($checkSequence)) { + $schema->createSequence($checkSequence); + } + } + } + } + + return $schema->toDropSql($this->platform); + } + + /** + * Updates the database schema of the given classes by comparing the ClassMetadata + * instances to the current database schema that is inspected. + * + * @param mixed[] $classes + */ + public function updateSchema(array $classes): void + { + $conn = $this->em->getConnection(); + + foreach ($this->getUpdateSchemaSql($classes) as $sql) { + $conn->executeStatement($sql); + } + } + + /** + * Gets the sequence of SQL statements that need to be performed in order + * to bring the given class mappings in-synch with the relational schema. + * + * @param list $classes The classes to consider. + * + * @return list The sequence of SQL statements. + */ + public function getUpdateSchemaSql(array $classes): array + { + $toSchema = $this->getSchemaFromMetadata($classes); + $fromSchema = $this->createSchemaForComparison($toSchema); + $comparator = $this->schemaManager->createComparator(); + $schemaDiff = $comparator->compareSchemas($fromSchema, $toSchema); + + return $this->platform->getAlterSchemaSQL($schemaDiff); + } + + /** + * Creates the schema from the database, ensuring tables from the target schema are whitelisted for comparison. + */ + private function createSchemaForComparison(Schema $toSchema): Schema + { + $connection = $this->em->getConnection(); + + // backup schema assets filter + $config = $connection->getConfiguration(); + $previousFilter = $config->getSchemaAssetsFilter(); + + if ($previousFilter === null) { + return $this->schemaManager->introspectSchema(); + } + + // whitelist assets we already know about in $toSchema, use the existing filter otherwise + $config->setSchemaAssetsFilter(static function ($asset) use ($previousFilter, $toSchema): bool { + $assetName = $asset instanceof AbstractAsset ? $asset->getName() : $asset; + + return $toSchema->hasTable($assetName) || $toSchema->hasSequence($assetName) || $previousFilter($asset); + }); + + try { + return $this->schemaManager->introspectSchema(); + } finally { + // restore schema assets filter + $config->setSchemaAssetsFilter($previousFilter); + } + } +} diff --git a/vendor/doctrine/orm/src/Tools/SchemaValidator.php b/vendor/doctrine/orm/src/Tools/SchemaValidator.php new file mode 100644 index 0000000..fdfc003 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/SchemaValidator.php @@ -0,0 +1,443 @@ + ['string'], + BigIntType::class => ['int', 'string'], + BooleanType::class => ['bool'], + DecimalType::class => ['string'], + FloatType::class => ['float'], + GuidType::class => ['string'], + IntegerType::class => ['int'], + JsonType::class => ['array'], + SimpleArrayType::class => ['array'], + SmallIntType::class => ['int'], + StringType::class => ['string'], + TextType::class => ['string'], + ]; + + public function __construct( + private readonly EntityManagerInterface $em, + private readonly bool $validatePropertyTypes = true, + ) { + } + + /** + * Checks the internal consistency of all mapping files. + * + * There are several checks that can't be done at runtime or are too expensive, which can be verified + * with this command. For example: + * + * 1. Check if a relation with "mappedBy" is actually connected to that specified field. + * 2. Check if "mappedBy" and "inversedBy" are consistent to each other. + * 3. Check if "referencedColumnName" attributes are really pointing to primary key columns. + * + * @psalm-return array> + */ + public function validateMapping(): array + { + $errors = []; + $cmf = $this->em->getMetadataFactory(); + $classes = $cmf->getAllMetadata(); + + foreach ($classes as $class) { + $ce = $this->validateClass($class); + if ($ce) { + $errors[$class->name] = $ce; + } + } + + return $errors; + } + + /** + * Validates a single class of the current. + * + * @return string[] + * @psalm-return list + */ + public function validateClass(ClassMetadata $class): array + { + $ce = []; + $cmf = $this->em->getMetadataFactory(); + + foreach ($class->fieldMappings as $fieldName => $mapping) { + if (! Type::hasType($mapping->type)) { + $ce[] = "The field '" . $class->name . '#' . $fieldName . "' uses a non-existent type '" . $mapping->type . "'."; + } + } + + if ($this->validatePropertyTypes) { + array_push($ce, ...$this->validatePropertiesTypes($class)); + } + + foreach ($class->associationMappings as $fieldName => $assoc) { + if (! class_exists($assoc->targetEntity) || $cmf->isTransient($assoc->targetEntity)) { + $ce[] = "The target entity '" . $assoc->targetEntity . "' specified on " . $class->name . '#' . $fieldName . ' is unknown or not an entity.'; + + return $ce; + } + + $targetMetadata = $cmf->getMetadataFor($assoc->targetEntity); + + if ($targetMetadata->isMappedSuperclass) { + $ce[] = "The target entity '" . $assoc->targetEntity . "' specified on " . $class->name . '#' . $fieldName . ' is a mapped superclass. This is not possible since there is no table that a foreign key could refer to.'; + + return $ce; + } + + if (isset($assoc->id) && $targetMetadata->containsForeignIdentifier) { + $ce[] = "Cannot map association '" . $class->name . '#' . $fieldName . ' as identifier, because ' . + "the target entity '" . $targetMetadata->name . "' also maps an association as identifier."; + } + + if (! $assoc->isOwningSide()) { + if ($targetMetadata->hasField($assoc->mappedBy)) { + $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the owning side ' . + 'field ' . $assoc->targetEntity . '#' . $assoc->mappedBy . ' which is not defined as association, but as field.'; + } + + if (! $targetMetadata->hasAssociation($assoc->mappedBy)) { + $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the owning side ' . + 'field ' . $assoc->targetEntity . '#' . $assoc->mappedBy . ' which does not exist.'; + } elseif ($targetMetadata->associationMappings[$assoc->mappedBy]->inversedBy === null) { + $ce[] = 'The field ' . $class->name . '#' . $fieldName . ' is on the inverse side of a ' . + 'bi-directional relationship, but the specified mappedBy association on the target-entity ' . + $assoc->targetEntity . '#' . $assoc->mappedBy . ' does not contain the required ' . + "'inversedBy=\"" . $fieldName . "\"' attribute."; + } elseif ($targetMetadata->associationMappings[$assoc->mappedBy]->inversedBy !== $fieldName) { + $ce[] = 'The mappings ' . $class->name . '#' . $fieldName . ' and ' . + $assoc->targetEntity . '#' . $assoc->mappedBy . ' are ' . + 'inconsistent with each other.'; + } + } + + if ($assoc->isOwningSide() && $assoc->inversedBy !== null) { + if ($targetMetadata->hasField($assoc->inversedBy)) { + $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the inverse side ' . + 'field ' . $assoc->targetEntity . '#' . $assoc->inversedBy . ' which is not defined as association.'; + } + + if (! $targetMetadata->hasAssociation($assoc->inversedBy)) { + $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the inverse side ' . + 'field ' . $assoc->targetEntity . '#' . $assoc->inversedBy . ' which does not exist.'; + } elseif ($targetMetadata->associationMappings[$assoc->inversedBy]->isOwningSide()) { + $ce[] = 'The field ' . $class->name . '#' . $fieldName . ' is on the owning side of a ' . + 'bi-directional relationship, but the specified inversedBy association on the target-entity ' . + $assoc->targetEntity . '#' . $assoc->inversedBy . ' does not contain the required ' . + "'mappedBy=\"" . $fieldName . "\"' attribute."; + } elseif ($targetMetadata->associationMappings[$assoc->inversedBy]->mappedBy !== $fieldName) { + $ce[] = 'The mappings ' . $class->name . '#' . $fieldName . ' and ' . + $assoc->targetEntity . '#' . $assoc->inversedBy . ' are ' . + 'inconsistent with each other.'; + } + + // Verify inverse side/owning side match each other + if (array_key_exists($assoc->inversedBy, $targetMetadata->associationMappings)) { + $targetAssoc = $targetMetadata->associationMappings[$assoc->inversedBy]; + if ($assoc->isOneToOne() && ! $targetAssoc->isOneToOne()) { + $ce[] = 'If association ' . $class->name . '#' . $fieldName . ' is one-to-one, then the inversed ' . + 'side ' . $targetMetadata->name . '#' . $assoc->inversedBy . ' has to be one-to-one as well.'; + } elseif ($assoc->isManyToOne() && ! $targetAssoc->isOneToMany()) { + $ce[] = 'If association ' . $class->name . '#' . $fieldName . ' is many-to-one, then the inversed ' . + 'side ' . $targetMetadata->name . '#' . $assoc->inversedBy . ' has to be one-to-many.'; + } elseif ($assoc->isManyToMany() && ! $targetAssoc->isManyToMany()) { + $ce[] = 'If association ' . $class->name . '#' . $fieldName . ' is many-to-many, then the inversed ' . + 'side ' . $targetMetadata->name . '#' . $assoc->inversedBy . ' has to be many-to-many as well.'; + } + } + } + + if ($assoc->isOwningSide()) { + if ($assoc->isManyToManyOwningSide()) { + $identifierColumns = $class->getIdentifierColumnNames(); + foreach ($assoc->joinTable->joinColumns as $joinColumn) { + if (! in_array($joinColumn->referencedColumnName, $identifierColumns, true)) { + $ce[] = "The referenced column name '" . $joinColumn->referencedColumnName . "' " . + "has to be a primary key column on the target entity class '" . $class->name . "'."; + break; + } + } + + $identifierColumns = $targetMetadata->getIdentifierColumnNames(); + foreach ($assoc->joinTable->inverseJoinColumns as $inverseJoinColumn) { + if (! in_array($inverseJoinColumn->referencedColumnName, $identifierColumns, true)) { + $ce[] = "The referenced column name '" . $inverseJoinColumn->referencedColumnName . "' " . + "has to be a primary key column on the target entity class '" . $targetMetadata->name . "'."; + break; + } + } + + if (count($targetMetadata->getIdentifierColumnNames()) !== count($assoc->joinTable->inverseJoinColumns)) { + $ce[] = "The inverse join columns of the many-to-many table '" . $assoc->joinTable->name . "' " . + "have to contain to ALL identifier columns of the target entity '" . $targetMetadata->name . "', " . + "however '" . implode(', ', array_diff($targetMetadata->getIdentifierColumnNames(), array_values($assoc->relationToTargetKeyColumns))) . + "' are missing."; + } + + if (count($class->getIdentifierColumnNames()) !== count($assoc->joinTable->joinColumns)) { + $ce[] = "The join columns of the many-to-many table '" . $assoc->joinTable->name . "' " . + "have to contain to ALL identifier columns of the source entity '" . $class->name . "', " . + "however '" . implode(', ', array_diff($class->getIdentifierColumnNames(), array_values($assoc->relationToSourceKeyColumns))) . + "' are missing."; + } + } elseif ($assoc->isToOneOwningSide()) { + $identifierColumns = $targetMetadata->getIdentifierColumnNames(); + foreach ($assoc->joinColumns as $joinColumn) { + if (! in_array($joinColumn->referencedColumnName, $identifierColumns, true)) { + $ce[] = "The referenced column name '" . $joinColumn->referencedColumnName . "' " . + "has to be a primary key column on the target entity class '" . $targetMetadata->name . "'."; + } + } + + if (count($identifierColumns) !== count($assoc->joinColumns)) { + $ids = []; + + foreach ($assoc->joinColumns as $joinColumn) { + $ids[] = $joinColumn->name; + } + + $ce[] = "The join columns of the association '" . $assoc->fieldName . "' " . + "have to match to ALL identifier columns of the target entity '" . $targetMetadata->name . "', " . + "however '" . implode(', ', array_diff($targetMetadata->getIdentifierColumnNames(), $ids)) . + "' are missing."; + } + } + } + + if ($assoc->isOrdered()) { + foreach ($assoc->orderBy() as $orderField => $orientation) { + if (! $targetMetadata->hasField($orderField) && ! $targetMetadata->hasAssociation($orderField)) { + $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' is ordered by a foreign field ' . + $orderField . ' that is not a field on the target entity ' . $targetMetadata->name . '.'; + continue; + } + + if ($targetMetadata->isCollectionValuedAssociation($orderField)) { + $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' is ordered by a field ' . + $orderField . ' on ' . $targetMetadata->name . ' that is a collection-valued association.'; + continue; + } + + if ($targetMetadata->isAssociationInverseSide($orderField)) { + $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' is ordered by a field ' . + $orderField . ' on ' . $targetMetadata->name . ' that is the inverse side of an association.'; + continue; + } + } + } + } + + if ( + ! $class->isInheritanceTypeNone() + && ! $class->isRootEntity() + && ($class->reflClass !== null && ! $class->reflClass->isAbstract()) + && ! $class->isMappedSuperclass + && array_search($class->name, $class->discriminatorMap, true) === false + ) { + $ce[] = "Entity class '" . $class->name . "' is part of inheritance hierarchy, but is " . + "not mapped in the root entity '" . $class->rootEntityName . "' discriminator map. " . + 'All subclasses must be listed in the discriminator map.'; + } + + foreach ($class->subClasses as $subClass) { + if (! in_array($class->name, class_parents($subClass), true)) { + $ce[] = "According to the discriminator map class '" . $subClass . "' has to be a child " . + "of '" . $class->name . "' but these entities are not related through inheritance."; + } + } + + return $ce; + } + + /** + * Checks if the Database Schema is in sync with the current metadata state. + */ + public function schemaInSyncWithMetadata(): bool + { + return count($this->getUpdateSchemaList()) === 0; + } + + /** + * Returns the list of missing Database Schema updates. + * + * @return array + */ + public function getUpdateSchemaList(): array + { + $schemaTool = new SchemaTool($this->em); + + $allMetadata = $this->em->getMetadataFactory()->getAllMetadata(); + + return $schemaTool->getUpdateSchemaSql($allMetadata); + } + + /** @return list containing the found issues */ + private function validatePropertiesTypes(ClassMetadata $class): array + { + return array_values( + array_filter( + array_map( + function (FieldMapping $fieldMapping) use ($class): string|null { + $fieldName = $fieldMapping->fieldName; + assert(isset($class->reflFields[$fieldName])); + $propertyType = $class->reflFields[$fieldName]->getType(); + + // If the field type is not a built-in type, we cannot check it + if (! Type::hasType($fieldMapping->type)) { + return null; + } + + // If the property type is not a named type, we cannot check it + if (! ($propertyType instanceof ReflectionNamedType) || $propertyType->getName() === 'mixed') { + return null; + } + + $metadataFieldType = $this->findBuiltInType(Type::getType($fieldMapping->type)); + + //If the metadata field type is not a mapped built-in type, we cannot check it + if ($metadataFieldType === null) { + return null; + } + + $propertyType = $propertyType->getName(); + + // If the property type is the same as the metadata field type, we are ok + if (in_array($propertyType, $metadataFieldType, true)) { + return null; + } + + if (is_a($propertyType, BackedEnum::class, true)) { + $backingType = (string) (new ReflectionEnum($propertyType))->getBackingType(); + + if (! in_array($backingType, $metadataFieldType, true)) { + return sprintf( + "The field '%s#%s' has the property type '%s' with a backing type of '%s' that differs from the metadata field type '%s'.", + $class->name, + $fieldName, + $propertyType, + $backingType, + implode('|', $metadataFieldType), + ); + } + + if (! isset($fieldMapping->enumType) || $propertyType === $fieldMapping->enumType) { + return null; + } + + return sprintf( + "The field '%s#%s' has the property type '%s' that differs from the metadata enumType '%s'.", + $class->name, + $fieldName, + $propertyType, + $fieldMapping->enumType, + ); + } + + if ( + isset($fieldMapping->enumType) + && $propertyType !== $fieldMapping->enumType + && interface_exists($propertyType) + && is_a($fieldMapping->enumType, $propertyType, true) + ) { + $backingType = (string) (new ReflectionEnum($fieldMapping->enumType))->getBackingType(); + + if (in_array($backingType, $metadataFieldType, true)) { + return null; + } + + return sprintf( + "The field '%s#%s' has the metadata enumType '%s' with a backing type of '%s' that differs from the metadata field type '%s'.", + $class->name, + $fieldName, + $fieldMapping->enumType, + $backingType, + implode('|', $metadataFieldType), + ); + } + + if ( + $fieldMapping->type === 'json' + && in_array($propertyType, ['string', 'int', 'float', 'bool', 'true', 'false', 'null'], true) + ) { + return null; + } + + return sprintf( + "The field '%s#%s' has the property type '%s' that differs from the metadata field type '%s' returned by the '%s' DBAL type.", + $class->name, + $fieldName, + $propertyType, + implode('|', $metadataFieldType), + $fieldMapping->type, + ); + }, + $class->fieldMappings, + ), + ), + ); + } + + /** + * The exact DBAL type must be used (no subclasses), since consumers of doctrine/orm may have their own + * customization around field types. + * + * @return list|null + */ + private function findBuiltInType(Type $type): array|null + { + $typeName = $type::class; + + return self::BUILTIN_TYPES_MAP[$typeName] ?? null; + } +} diff --git a/vendor/doctrine/orm/src/Tools/ToolEvents.php b/vendor/doctrine/orm/src/Tools/ToolEvents.php new file mode 100644 index 0000000..fac37fa --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/ToolEvents.php @@ -0,0 +1,23 @@ +getMessage() . "' while executing DDL: " . $sql, + 0, + $e, + ); + } +} -- cgit v1.2.3