diff options
Diffstat (limited to 'vendor/doctrine/orm/src/Tools')
43 files changed, 5236 insertions, 0 deletions
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Event\LoadClassMetadataEventArgs; | ||
| 8 | use Doctrine\ORM\Events; | ||
| 9 | use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder; | ||
| 10 | |||
| 11 | use function assert; | ||
| 12 | use function ltrim; | ||
| 13 | |||
| 14 | /** | ||
| 15 | * Mechanism to programmatically attach entity listeners. | ||
| 16 | */ | ||
| 17 | class AttachEntityListenersListener | ||
| 18 | { | ||
| 19 | /** | ||
| 20 | * @var array<class-string, list<array{ | ||
| 21 | * event: Events::*|null, | ||
| 22 | * class: class-string, | ||
| 23 | * method: string|null, | ||
| 24 | * }>> | ||
| 25 | */ | ||
| 26 | private array $entityListeners = []; | ||
| 27 | |||
| 28 | /** | ||
| 29 | * Adds an entity listener for a specific entity. | ||
| 30 | * | ||
| 31 | * @param class-string $entityClass The entity to attach the listener. | ||
| 32 | * @param class-string $listenerClass The listener class. | ||
| 33 | * @param Events::*|null $eventName The entity lifecycle event. | ||
| 34 | * @param non-falsy-string|null $listenerCallback The listener callback method or NULL to use $eventName. | ||
| 35 | */ | ||
| 36 | public function addEntityListener( | ||
| 37 | string $entityClass, | ||
| 38 | string $listenerClass, | ||
| 39 | string|null $eventName = null, | ||
| 40 | string|null $listenerCallback = null, | ||
| 41 | ): void { | ||
| 42 | $this->entityListeners[ltrim($entityClass, '\\')][] = [ | ||
| 43 | 'event' => $eventName, | ||
| 44 | 'class' => $listenerClass, | ||
| 45 | 'method' => $listenerCallback ?? $eventName, | ||
| 46 | ]; | ||
| 47 | } | ||
| 48 | |||
| 49 | /** | ||
| 50 | * Processes event and attach the entity listener. | ||
| 51 | */ | ||
| 52 | public function loadClassMetadata(LoadClassMetadataEventArgs $event): void | ||
| 53 | { | ||
| 54 | $metadata = $event->getClassMetadata(); | ||
| 55 | |||
| 56 | if (! isset($this->entityListeners[$metadata->name])) { | ||
| 57 | return; | ||
| 58 | } | ||
| 59 | |||
| 60 | foreach ($this->entityListeners[$metadata->name] as $listener) { | ||
| 61 | if ($listener['event'] === null) { | ||
| 62 | EntityListenerBuilder::bindEntityListener($metadata, $listener['class']); | ||
| 63 | } else { | ||
| 64 | assert($listener['method'] !== null); | ||
| 65 | $metadata->addEntityListener($listener['event'], $listener['class'], $listener['method']); | ||
| 66 | } | ||
| 67 | } | ||
| 68 | } | ||
| 69 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\ORM\Tools\Console\EntityManagerProvider; | ||
| 9 | use Symfony\Component\Console\Command\Command; | ||
| 10 | use Symfony\Component\Console\Input\InputInterface; | ||
| 11 | |||
| 12 | abstract class AbstractEntityManagerCommand extends Command | ||
| 13 | { | ||
| 14 | public function __construct(private readonly EntityManagerProvider $entityManagerProvider) | ||
| 15 | { | ||
| 16 | parent::__construct(); | ||
| 17 | } | ||
| 18 | |||
| 19 | final protected function getEntityManager(InputInterface $input): EntityManagerInterface | ||
| 20 | { | ||
| 21 | return $input->getOption('em') === null | ||
| 22 | ? $this->entityManagerProvider->getDefaultManager() | ||
| 23 | : $this->entityManagerProvider->getManager($input->getOption('em')); | ||
| 24 | } | ||
| 25 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\ClearCache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache; | ||
| 8 | use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand; | ||
| 9 | use InvalidArgumentException; | ||
| 10 | use Symfony\Component\Console\Input\InputArgument; | ||
| 11 | use Symfony\Component\Console\Input\InputInterface; | ||
| 12 | use Symfony\Component\Console\Input\InputOption; | ||
| 13 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 14 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 15 | |||
| 16 | use function sprintf; | ||
| 17 | |||
| 18 | /** | ||
| 19 | * Command to clear a collection cache region. | ||
| 20 | */ | ||
| 21 | class CollectionRegionCommand extends AbstractEntityManagerCommand | ||
| 22 | { | ||
| 23 | protected function configure(): void | ||
| 24 | { | ||
| 25 | $this->setName('orm:clear-cache:region:collection') | ||
| 26 | ->setDescription('Clear a second-level cache collection region') | ||
| 27 | ->addArgument('owner-class', InputArgument::OPTIONAL, 'The owner entity name.') | ||
| 28 | ->addArgument('association', InputArgument::OPTIONAL, 'The association collection name.') | ||
| 29 | ->addArgument('owner-id', InputArgument::OPTIONAL, 'The owner identifier.') | ||
| 30 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 31 | ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all entity regions will be deleted/invalidated.') | ||
| 32 | ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, all cache entries will be flushed.') | ||
| 33 | ->setHelp(<<<'EOT' | ||
| 34 | The <info>%command.name%</info> command is meant to clear a second-level cache collection regions for an associated Entity Manager. | ||
| 35 | It is possible to delete/invalidate all collection region, a specific collection region or flushes the cache provider. | ||
| 36 | |||
| 37 | The execution type differ on how you execute the command. | ||
| 38 | If you want to invalidate all entries for an collection region this command would do the work: | ||
| 39 | |||
| 40 | <info>%command.name% 'Entities\MyEntity' 'collectionName'</info> | ||
| 41 | |||
| 42 | To invalidate a specific entry you should use : | ||
| 43 | |||
| 44 | <info>%command.name% 'Entities\MyEntity' 'collectionName' 1</info> | ||
| 45 | |||
| 46 | If you want to invalidate all entries for the all collection regions: | ||
| 47 | |||
| 48 | <info>%command.name% --all</info> | ||
| 49 | |||
| 50 | Alternatively, if you want to flush the configured cache provider for an collection region use this command: | ||
| 51 | |||
| 52 | <info>%command.name% 'Entities\MyEntity' 'collectionName' --flush</info> | ||
| 53 | |||
| 54 | Finally, be aware that if <info>--flush</info> option is passed, | ||
| 55 | not all cache providers are able to flush entries, because of a limitation of its execution nature. | ||
| 56 | EOT); | ||
| 57 | } | ||
| 58 | |||
| 59 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 60 | { | ||
| 61 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 62 | |||
| 63 | $em = $this->getEntityManager($input); | ||
| 64 | $ownerClass = $input->getArgument('owner-class'); | ||
| 65 | $assoc = $input->getArgument('association'); | ||
| 66 | $ownerId = $input->getArgument('owner-id'); | ||
| 67 | $cache = $em->getCache(); | ||
| 68 | |||
| 69 | if (! $cache instanceof Cache) { | ||
| 70 | throw new InvalidArgumentException('No second-level cache is configured on the given EntityManager.'); | ||
| 71 | } | ||
| 72 | |||
| 73 | if (( ! $ownerClass || ! $assoc) && ! $input->getOption('all')) { | ||
| 74 | throw new InvalidArgumentException('Missing arguments "--owner-class" "--association"'); | ||
| 75 | } | ||
| 76 | |||
| 77 | if ($input->getOption('flush')) { | ||
| 78 | $cache->getCollectionCacheRegion($ownerClass, $assoc) | ||
| 79 | ->evictAll(); | ||
| 80 | |||
| 81 | $ui->comment( | ||
| 82 | sprintf( | ||
| 83 | 'Flushing cache provider configured for <info>"%s#%s"</info>', | ||
| 84 | $ownerClass, | ||
| 85 | $assoc, | ||
| 86 | ), | ||
| 87 | ); | ||
| 88 | |||
| 89 | return 0; | ||
| 90 | } | ||
| 91 | |||
| 92 | if ($input->getOption('all')) { | ||
| 93 | $ui->comment('Clearing <info>all</info> second-level cache collection regions'); | ||
| 94 | |||
| 95 | $cache->evictEntityRegions(); | ||
| 96 | |||
| 97 | return 0; | ||
| 98 | } | ||
| 99 | |||
| 100 | if ($ownerId) { | ||
| 101 | $ui->comment( | ||
| 102 | sprintf( | ||
| 103 | 'Clearing second-level cache entry for collection <info>"%s#%s"</info> owner entity identified by <info>"%s"</info>', | ||
| 104 | $ownerClass, | ||
| 105 | $assoc, | ||
| 106 | $ownerId, | ||
| 107 | ), | ||
| 108 | ); | ||
| 109 | $cache->evictCollection($ownerClass, $assoc, $ownerId); | ||
| 110 | |||
| 111 | return 0; | ||
| 112 | } | ||
| 113 | |||
| 114 | $ui->comment(sprintf('Clearing second-level cache for collection <info>"%s#%s"</info>', $ownerClass, $assoc)); | ||
| 115 | $cache->evictCollectionRegion($ownerClass, $assoc); | ||
| 116 | |||
| 117 | return 0; | ||
| 118 | } | ||
| 119 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\ClearCache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache; | ||
| 8 | use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand; | ||
| 9 | use InvalidArgumentException; | ||
| 10 | use Symfony\Component\Console\Input\InputArgument; | ||
| 11 | use Symfony\Component\Console\Input\InputInterface; | ||
| 12 | use Symfony\Component\Console\Input\InputOption; | ||
| 13 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 14 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 15 | |||
| 16 | use function sprintf; | ||
| 17 | |||
| 18 | /** | ||
| 19 | * Command to clear a entity cache region. | ||
| 20 | */ | ||
| 21 | class EntityRegionCommand extends AbstractEntityManagerCommand | ||
| 22 | { | ||
| 23 | protected function configure(): void | ||
| 24 | { | ||
| 25 | $this->setName('orm:clear-cache:region:entity') | ||
| 26 | ->setDescription('Clear a second-level cache entity region') | ||
| 27 | ->addArgument('entity-class', InputArgument::OPTIONAL, 'The entity name.') | ||
| 28 | ->addArgument('entity-id', InputArgument::OPTIONAL, 'The entity identifier.') | ||
| 29 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 30 | ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all entity regions will be deleted/invalidated.') | ||
| 31 | ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, all cache entries will be flushed.') | ||
| 32 | ->setHelp(<<<'EOT' | ||
| 33 | The <info>%command.name%</info> command is meant to clear a second-level cache entity region for an associated Entity Manager. | ||
| 34 | It is possible to delete/invalidate all entity region, a specific entity region or flushes the cache provider. | ||
| 35 | |||
| 36 | The execution type differ on how you execute the command. | ||
| 37 | If you want to invalidate all entries for an entity region this command would do the work: | ||
| 38 | |||
| 39 | <info>%command.name% 'Entities\MyEntity'</info> | ||
| 40 | |||
| 41 | To invalidate a specific entry you should use : | ||
| 42 | |||
| 43 | <info>%command.name% 'Entities\MyEntity' 1</info> | ||
| 44 | |||
| 45 | If you want to invalidate all entries for the all entity regions: | ||
| 46 | |||
| 47 | <info>%command.name% --all</info> | ||
| 48 | |||
| 49 | Alternatively, if you want to flush the configured cache provider for an entity region use this command: | ||
| 50 | |||
| 51 | <info>%command.name% 'Entities\MyEntity' --flush</info> | ||
| 52 | |||
| 53 | Finally, be aware that if <info>--flush</info> option is passed, | ||
| 54 | not all cache providers are able to flush entries, because of a limitation of its execution nature. | ||
| 55 | EOT); | ||
| 56 | } | ||
| 57 | |||
| 58 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 59 | { | ||
| 60 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 61 | |||
| 62 | $em = $this->getEntityManager($input); | ||
| 63 | $entityClass = $input->getArgument('entity-class'); | ||
| 64 | $entityId = $input->getArgument('entity-id'); | ||
| 65 | $cache = $em->getCache(); | ||
| 66 | |||
| 67 | if (! $cache instanceof Cache) { | ||
| 68 | throw new InvalidArgumentException('No second-level cache is configured on the given EntityManager.'); | ||
| 69 | } | ||
| 70 | |||
| 71 | if (! $entityClass && ! $input->getOption('all')) { | ||
| 72 | throw new InvalidArgumentException('Invalid argument "--entity-class"'); | ||
| 73 | } | ||
| 74 | |||
| 75 | if ($input->getOption('flush')) { | ||
| 76 | $cache->getEntityCacheRegion($entityClass) | ||
| 77 | ->evictAll(); | ||
| 78 | |||
| 79 | $ui->comment(sprintf('Flushing cache provider configured for entity named <info>"%s"</info>', $entityClass)); | ||
| 80 | |||
| 81 | return 0; | ||
| 82 | } | ||
| 83 | |||
| 84 | if ($input->getOption('all')) { | ||
| 85 | $ui->comment('Clearing <info>all</info> second-level cache entity regions'); | ||
| 86 | |||
| 87 | $cache->evictEntityRegions(); | ||
| 88 | |||
| 89 | return 0; | ||
| 90 | } | ||
| 91 | |||
| 92 | if ($entityId) { | ||
| 93 | $ui->comment( | ||
| 94 | sprintf( | ||
| 95 | 'Clearing second-level cache entry for entity <info>"%s"</info> identified by <info>"%s"</info>', | ||
| 96 | $entityClass, | ||
| 97 | $entityId, | ||
| 98 | ), | ||
| 99 | ); | ||
| 100 | $cache->evictEntity($entityClass, $entityId); | ||
| 101 | |||
| 102 | return 0; | ||
| 103 | } | ||
| 104 | |||
| 105 | $ui->comment(sprintf('Clearing second-level cache for entity <info>"%s"</info>', $entityClass)); | ||
| 106 | $cache->evictEntityRegion($entityClass); | ||
| 107 | |||
| 108 | return 0; | ||
| 109 | } | ||
| 110 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\ClearCache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand; | ||
| 8 | use InvalidArgumentException; | ||
| 9 | use Symfony\Component\Console\Input\InputInterface; | ||
| 10 | use Symfony\Component\Console\Input\InputOption; | ||
| 11 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 12 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 13 | |||
| 14 | /** | ||
| 15 | * Command to clear the metadata cache of the various cache drivers. | ||
| 16 | * | ||
| 17 | * @link www.doctrine-project.org | ||
| 18 | */ | ||
| 19 | class MetadataCommand extends AbstractEntityManagerCommand | ||
| 20 | { | ||
| 21 | protected function configure(): void | ||
| 22 | { | ||
| 23 | $this->setName('orm:clear-cache:metadata') | ||
| 24 | ->setDescription('Clear all metadata cache of the various cache drivers') | ||
| 25 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 26 | ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, cache entries will be flushed instead of deleted/invalidated.') | ||
| 27 | ->setHelp(<<<'EOT' | ||
| 28 | The <info>%command.name%</info> command is meant to clear the metadata cache of associated Entity Manager. | ||
| 29 | EOT); | ||
| 30 | } | ||
| 31 | |||
| 32 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 33 | { | ||
| 34 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 35 | |||
| 36 | $em = $this->getEntityManager($input); | ||
| 37 | $cacheDriver = $em->getConfiguration()->getMetadataCache(); | ||
| 38 | |||
| 39 | if (! $cacheDriver) { | ||
| 40 | throw new InvalidArgumentException('No Metadata cache driver is configured on given EntityManager.'); | ||
| 41 | } | ||
| 42 | |||
| 43 | $ui->comment('Clearing <info>all</info> Metadata cache entries'); | ||
| 44 | |||
| 45 | $result = $cacheDriver->clear(); | ||
| 46 | $message = $result ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.'; | ||
| 47 | |||
| 48 | $ui->success($message); | ||
| 49 | |||
| 50 | return 0; | ||
| 51 | } | ||
| 52 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\ClearCache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand; | ||
| 8 | use InvalidArgumentException; | ||
| 9 | use LogicException; | ||
| 10 | use Symfony\Component\Cache\Adapter\ApcuAdapter; | ||
| 11 | use Symfony\Component\Console\Input\InputInterface; | ||
| 12 | use Symfony\Component\Console\Input\InputOption; | ||
| 13 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 14 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Command to clear the query cache of the various cache drivers. | ||
| 18 | * | ||
| 19 | * @link www.doctrine-project.org | ||
| 20 | */ | ||
| 21 | class QueryCommand extends AbstractEntityManagerCommand | ||
| 22 | { | ||
| 23 | protected function configure(): void | ||
| 24 | { | ||
| 25 | $this->setName('orm:clear-cache:query') | ||
| 26 | ->setDescription('Clear all query cache of the various cache drivers') | ||
| 27 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 28 | ->setHelp('The <info>%command.name%</info> command is meant to clear the query cache of associated Entity Manager.'); | ||
| 29 | } | ||
| 30 | |||
| 31 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 32 | { | ||
| 33 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 34 | |||
| 35 | $em = $this->getEntityManager($input); | ||
| 36 | $cache = $em->getConfiguration()->getQueryCache(); | ||
| 37 | |||
| 38 | if (! $cache) { | ||
| 39 | throw new InvalidArgumentException('No Query cache driver is configured on given EntityManager.'); | ||
| 40 | } | ||
| 41 | |||
| 42 | if ($cache instanceof ApcuAdapter) { | ||
| 43 | throw new LogicException('Cannot clear APCu Cache from Console, it\'s shared in the Webserver memory and not accessible from the CLI.'); | ||
| 44 | } | ||
| 45 | |||
| 46 | $ui->comment('Clearing <info>all</info> Query cache entries'); | ||
| 47 | |||
| 48 | $message = $cache->clear() ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.'; | ||
| 49 | |||
| 50 | $ui->success($message); | ||
| 51 | |||
| 52 | return 0; | ||
| 53 | } | ||
| 54 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\ClearCache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache; | ||
| 8 | use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand; | ||
| 9 | use InvalidArgumentException; | ||
| 10 | use Symfony\Component\Console\Input\InputArgument; | ||
| 11 | use Symfony\Component\Console\Input\InputInterface; | ||
| 12 | use Symfony\Component\Console\Input\InputOption; | ||
| 13 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 14 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 15 | |||
| 16 | use function sprintf; | ||
| 17 | |||
| 18 | /** | ||
| 19 | * Command to clear a query cache region. | ||
| 20 | */ | ||
| 21 | class QueryRegionCommand extends AbstractEntityManagerCommand | ||
| 22 | { | ||
| 23 | protected function configure(): void | ||
| 24 | { | ||
| 25 | $this->setName('orm:clear-cache:region:query') | ||
| 26 | ->setDescription('Clear a second-level cache query region') | ||
| 27 | ->addArgument('region-name', InputArgument::OPTIONAL, 'The query region to clear.') | ||
| 28 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 29 | ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all query regions will be deleted/invalidated.') | ||
| 30 | ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, all cache entries will be flushed.') | ||
| 31 | ->setHelp(<<<'EOT' | ||
| 32 | The <info>%command.name%</info> command is meant to clear a second-level cache query region for an associated Entity Manager. | ||
| 33 | It is possible to delete/invalidate all query region, a specific query region or flushes the cache provider. | ||
| 34 | |||
| 35 | The execution type differ on how you execute the command. | ||
| 36 | If you want to invalidate all entries for the default query region this command would do the work: | ||
| 37 | |||
| 38 | <info>%command.name%</info> | ||
| 39 | |||
| 40 | To invalidate entries for a specific query region you should use : | ||
| 41 | |||
| 42 | <info>%command.name% my_region_name</info> | ||
| 43 | |||
| 44 | If you want to invalidate all entries for the all query region: | ||
| 45 | |||
| 46 | <info>%command.name% --all</info> | ||
| 47 | |||
| 48 | Alternatively, if you want to flush the configured cache provider use this command: | ||
| 49 | |||
| 50 | <info>%command.name% my_region_name --flush</info> | ||
| 51 | |||
| 52 | Finally, be aware that if <info>--flush</info> option is passed, | ||
| 53 | not all cache providers are able to flush entries, because of a limitation of its execution nature. | ||
| 54 | EOT); | ||
| 55 | } | ||
| 56 | |||
| 57 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 58 | { | ||
| 59 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 60 | |||
| 61 | $em = $this->getEntityManager($input); | ||
| 62 | $name = $input->getArgument('region-name'); | ||
| 63 | $cache = $em->getCache(); | ||
| 64 | |||
| 65 | if ($name === null) { | ||
| 66 | $name = Cache::DEFAULT_QUERY_REGION_NAME; | ||
| 67 | } | ||
| 68 | |||
| 69 | if (! $cache instanceof Cache) { | ||
| 70 | throw new InvalidArgumentException('No second-level cache is configured on the given EntityManager.'); | ||
| 71 | } | ||
| 72 | |||
| 73 | if ($input->getOption('flush')) { | ||
| 74 | $cache->getQueryCache($name) | ||
| 75 | ->getRegion() | ||
| 76 | ->evictAll(); | ||
| 77 | |||
| 78 | $ui->comment( | ||
| 79 | sprintf( | ||
| 80 | 'Flushing cache provider configured for second-level cache query region named <info>"%s"</info>', | ||
| 81 | $name, | ||
| 82 | ), | ||
| 83 | ); | ||
| 84 | |||
| 85 | return 0; | ||
| 86 | } | ||
| 87 | |||
| 88 | if ($input->getOption('all')) { | ||
| 89 | $ui->comment('Clearing <info>all</info> second-level cache query regions'); | ||
| 90 | |||
| 91 | $cache->evictQueryRegions(); | ||
| 92 | |||
| 93 | return 0; | ||
| 94 | } | ||
| 95 | |||
| 96 | $ui->comment(sprintf('Clearing second-level cache query region named <info>"%s"</info>', $name)); | ||
| 97 | $cache->evictQueryRegion($name); | ||
| 98 | |||
| 99 | return 0; | ||
| 100 | } | ||
| 101 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\ClearCache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand; | ||
| 8 | use InvalidArgumentException; | ||
| 9 | use Symfony\Component\Console\Input\InputInterface; | ||
| 10 | use Symfony\Component\Console\Input\InputOption; | ||
| 11 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 12 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 13 | |||
| 14 | /** | ||
| 15 | * Command to clear the result cache of the various cache drivers. | ||
| 16 | * | ||
| 17 | * @link www.doctrine-project.org | ||
| 18 | */ | ||
| 19 | class ResultCommand extends AbstractEntityManagerCommand | ||
| 20 | { | ||
| 21 | protected function configure(): void | ||
| 22 | { | ||
| 23 | $this->setName('orm:clear-cache:result') | ||
| 24 | ->setDescription('Clear all result cache of the various cache drivers') | ||
| 25 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 26 | ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, cache entries will be flushed instead of deleted/invalidated.') | ||
| 27 | ->setHelp(<<<'EOT' | ||
| 28 | The <info>%command.name%</info> command is meant to clear the result cache of associated Entity Manager. | ||
| 29 | It is possible to invalidate all cache entries at once - called delete -, or flushes the cache provider | ||
| 30 | instance completely. | ||
| 31 | |||
| 32 | The execution type differ on how you execute the command. | ||
| 33 | If you want to invalidate the entries (and not delete from cache instance), this command would do the work: | ||
| 34 | |||
| 35 | <info>%command.name%</info> | ||
| 36 | |||
| 37 | Alternatively, if you want to flush the cache provider using this command: | ||
| 38 | |||
| 39 | <info>%command.name% --flush</info> | ||
| 40 | |||
| 41 | Finally, be aware that if <info>--flush</info> option is passed, not all cache providers are able to flush entries, | ||
| 42 | because of a limitation of its execution nature. | ||
| 43 | EOT); | ||
| 44 | } | ||
| 45 | |||
| 46 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 47 | { | ||
| 48 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 49 | |||
| 50 | $em = $this->getEntityManager($input); | ||
| 51 | $cache = $em->getConfiguration()->getResultCache(); | ||
| 52 | |||
| 53 | if (! $cache) { | ||
| 54 | throw new InvalidArgumentException('No Result cache driver is configured on given EntityManager.'); | ||
| 55 | } | ||
| 56 | |||
| 57 | $ui->comment('Clearing <info>all</info> Result cache entries'); | ||
| 58 | |||
| 59 | $message = $cache->clear() ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.'; | ||
| 60 | |||
| 61 | $ui->success($message); | ||
| 62 | |||
| 63 | return 0; | ||
| 64 | } | ||
| 65 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Tools\Console\MetadataFilter; | ||
| 8 | use InvalidArgumentException; | ||
| 9 | use Symfony\Component\Console\Input\InputArgument; | ||
| 10 | use Symfony\Component\Console\Input\InputInterface; | ||
| 11 | use Symfony\Component\Console\Input\InputOption; | ||
| 12 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 13 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 14 | |||
| 15 | use function file_exists; | ||
| 16 | use function is_dir; | ||
| 17 | use function is_writable; | ||
| 18 | use function mkdir; | ||
| 19 | use function realpath; | ||
| 20 | use function sprintf; | ||
| 21 | |||
| 22 | /** | ||
| 23 | * Command to (re)generate the proxy classes used by doctrine. | ||
| 24 | * | ||
| 25 | * @link www.doctrine-project.org | ||
| 26 | */ | ||
| 27 | class GenerateProxiesCommand extends AbstractEntityManagerCommand | ||
| 28 | { | ||
| 29 | protected function configure(): void | ||
| 30 | { | ||
| 31 | $this->setName('orm:generate-proxies') | ||
| 32 | ->setAliases(['orm:generate:proxies']) | ||
| 33 | ->setDescription('Generates proxy classes for entity classes') | ||
| 34 | ->addArgument('dest-path', InputArgument::OPTIONAL, 'The path to generate your proxy classes. If none is provided, it will attempt to grab from configuration.') | ||
| 35 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 36 | ->addOption('filter', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A string pattern used to match entities that should be processed.') | ||
| 37 | ->setHelp('Generates proxy classes for entity classes.'); | ||
| 38 | } | ||
| 39 | |||
| 40 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 41 | { | ||
| 42 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 43 | |||
| 44 | $em = $this->getEntityManager($input); | ||
| 45 | |||
| 46 | $metadatas = $em->getMetadataFactory()->getAllMetadata(); | ||
| 47 | $metadatas = MetadataFilter::filter($metadatas, $input->getOption('filter')); | ||
| 48 | |||
| 49 | // Process destination directory | ||
| 50 | $destPath = $input->getArgument('dest-path'); | ||
| 51 | if ($destPath === null) { | ||
| 52 | $destPath = $em->getConfiguration()->getProxyDir(); | ||
| 53 | |||
| 54 | if ($destPath === null) { | ||
| 55 | throw new InvalidArgumentException('Proxy directory cannot be null'); | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | if (! is_dir($destPath)) { | ||
| 60 | mkdir($destPath, 0775, true); | ||
| 61 | } | ||
| 62 | |||
| 63 | $destPath = realpath($destPath); | ||
| 64 | |||
| 65 | if (! file_exists($destPath)) { | ||
| 66 | throw new InvalidArgumentException( | ||
| 67 | sprintf("Proxies destination directory '<info>%s</info>' does not exist.", $em->getConfiguration()->getProxyDir()), | ||
| 68 | ); | ||
| 69 | } | ||
| 70 | |||
| 71 | if (! is_writable($destPath)) { | ||
| 72 | throw new InvalidArgumentException( | ||
| 73 | sprintf("Proxies destination directory '<info>%s</info>' does not have write permissions.", $destPath), | ||
| 74 | ); | ||
| 75 | } | ||
| 76 | |||
| 77 | if (empty($metadatas)) { | ||
| 78 | $ui->success('No Metadata Classes to process.'); | ||
| 79 | |||
| 80 | return 0; | ||
| 81 | } | ||
| 82 | |||
| 83 | foreach ($metadatas as $metadata) { | ||
| 84 | $ui->text(sprintf('Processing entity "<info>%s</info>"', $metadata->name)); | ||
| 85 | } | ||
| 86 | |||
| 87 | // Generating Proxies | ||
| 88 | $em->getProxyFactory()->generateProxyClasses($metadatas, $destPath); | ||
| 89 | |||
| 90 | // Outputting information message | ||
| 91 | $ui->newLine(); | ||
| 92 | $ui->text(sprintf('Proxy classes generated to "<info>%s</info>"', $destPath)); | ||
| 93 | |||
| 94 | return 0; | ||
| 95 | } | ||
| 96 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Mapping\MappingException; | ||
| 8 | use Symfony\Component\Console\Input\InputInterface; | ||
| 9 | use Symfony\Component\Console\Input\InputOption; | ||
| 10 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 11 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 12 | |||
| 13 | use function count; | ||
| 14 | use function sprintf; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Show information about mapped entities. | ||
| 18 | * | ||
| 19 | * @link www.doctrine-project.org | ||
| 20 | */ | ||
| 21 | class InfoCommand extends AbstractEntityManagerCommand | ||
| 22 | { | ||
| 23 | protected function configure(): void | ||
| 24 | { | ||
| 25 | $this->setName('orm:info') | ||
| 26 | ->setDescription('Show basic information about all mapped entities') | ||
| 27 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 28 | ->setHelp(<<<'EOT' | ||
| 29 | The <info>%command.name%</info> shows basic information about which | ||
| 30 | entities exist and possibly if their mapping information contains errors or | ||
| 31 | not. | ||
| 32 | EOT); | ||
| 33 | } | ||
| 34 | |||
| 35 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 36 | { | ||
| 37 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 38 | |||
| 39 | $entityManager = $this->getEntityManager($input); | ||
| 40 | |||
| 41 | $entityClassNames = $entityManager->getConfiguration() | ||
| 42 | ->getMetadataDriverImpl() | ||
| 43 | ->getAllClassNames(); | ||
| 44 | |||
| 45 | if (! $entityClassNames) { | ||
| 46 | $ui->caution( | ||
| 47 | [ | ||
| 48 | 'You do not have any mapped Doctrine ORM entities according to the current configuration.', | ||
| 49 | 'If you have entities or mapping files you should check your mapping configuration for errors.', | ||
| 50 | ], | ||
| 51 | ); | ||
| 52 | |||
| 53 | return 1; | ||
| 54 | } | ||
| 55 | |||
| 56 | $ui->text(sprintf('Found <info>%d</info> mapped entities:', count($entityClassNames))); | ||
| 57 | $ui->newLine(); | ||
| 58 | |||
| 59 | $failure = false; | ||
| 60 | |||
| 61 | foreach ($entityClassNames as $entityClassName) { | ||
| 62 | try { | ||
| 63 | $entityManager->getClassMetadata($entityClassName); | ||
| 64 | $ui->text(sprintf('<info>[OK]</info> %s', $entityClassName)); | ||
| 65 | } catch (MappingException $e) { | ||
| 66 | $ui->text( | ||
| 67 | [ | ||
| 68 | sprintf('<error>[FAIL]</error> %s', $entityClassName), | ||
| 69 | sprintf('<comment>%s</comment>', $e->getMessage()), | ||
| 70 | '', | ||
| 71 | ], | ||
| 72 | ); | ||
| 73 | |||
| 74 | $failure = true; | ||
| 75 | } | ||
| 76 | } | ||
| 77 | |||
| 78 | return $failure ? 1 : 0; | ||
| 79 | } | ||
| 80 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 9 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 10 | use Doctrine\ORM\Mapping\FieldMapping; | ||
| 11 | use Doctrine\Persistence\Mapping\MappingException; | ||
| 12 | use InvalidArgumentException; | ||
| 13 | use Symfony\Component\Console\Input\InputArgument; | ||
| 14 | use Symfony\Component\Console\Input\InputInterface; | ||
| 15 | use Symfony\Component\Console\Input\InputOption; | ||
| 16 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 17 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 18 | |||
| 19 | use function array_filter; | ||
| 20 | use function array_map; | ||
| 21 | use function array_merge; | ||
| 22 | use function count; | ||
| 23 | use function current; | ||
| 24 | use function get_debug_type; | ||
| 25 | use function implode; | ||
| 26 | use function is_array; | ||
| 27 | use function is_bool; | ||
| 28 | use function is_object; | ||
| 29 | use function is_scalar; | ||
| 30 | use function json_encode; | ||
| 31 | use function preg_match; | ||
| 32 | use function preg_quote; | ||
| 33 | use function print_r; | ||
| 34 | use function sprintf; | ||
| 35 | |||
| 36 | use const JSON_PRETTY_PRINT; | ||
| 37 | use const JSON_THROW_ON_ERROR; | ||
| 38 | use const JSON_UNESCAPED_SLASHES; | ||
| 39 | use const JSON_UNESCAPED_UNICODE; | ||
| 40 | |||
| 41 | /** | ||
| 42 | * Show information about mapped entities. | ||
| 43 | * | ||
| 44 | * @link www.doctrine-project.org | ||
| 45 | */ | ||
| 46 | final class MappingDescribeCommand extends AbstractEntityManagerCommand | ||
| 47 | { | ||
| 48 | protected function configure(): void | ||
| 49 | { | ||
| 50 | $this->setName('orm:mapping:describe') | ||
| 51 | ->addArgument('entityName', InputArgument::REQUIRED, 'Full or partial name of entity') | ||
| 52 | ->setDescription('Display information about mapped objects') | ||
| 53 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 54 | ->setHelp(<<<'EOT' | ||
| 55 | The %command.full_name% command describes the metadata for the given full or partial entity class name. | ||
| 56 | |||
| 57 | <info>%command.full_name%</info> My\Namespace\Entity\MyEntity | ||
| 58 | |||
| 59 | Or: | ||
| 60 | |||
| 61 | <info>%command.full_name%</info> MyEntity | ||
| 62 | EOT); | ||
| 63 | } | ||
| 64 | |||
| 65 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 66 | { | ||
| 67 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 68 | |||
| 69 | $entityManager = $this->getEntityManager($input); | ||
| 70 | |||
| 71 | $this->displayEntity($input->getArgument('entityName'), $entityManager, $ui); | ||
| 72 | |||
| 73 | return 0; | ||
| 74 | } | ||
| 75 | |||
| 76 | /** | ||
| 77 | * Display all the mapping information for a single Entity. | ||
| 78 | * | ||
| 79 | * @param string $entityName Full or partial entity class name | ||
| 80 | */ | ||
| 81 | private function displayEntity( | ||
| 82 | string $entityName, | ||
| 83 | EntityManagerInterface $entityManager, | ||
| 84 | SymfonyStyle $ui, | ||
| 85 | ): void { | ||
| 86 | $metadata = $this->getClassMetadata($entityName, $entityManager); | ||
| 87 | |||
| 88 | $ui->table( | ||
| 89 | ['Field', 'Value'], | ||
| 90 | array_merge( | ||
| 91 | [ | ||
| 92 | $this->formatField('Name', $metadata->name), | ||
| 93 | $this->formatField('Root entity name', $metadata->rootEntityName), | ||
| 94 | $this->formatField('Custom generator definition', $metadata->customGeneratorDefinition), | ||
| 95 | $this->formatField('Custom repository class', $metadata->customRepositoryClassName), | ||
| 96 | $this->formatField('Mapped super class?', $metadata->isMappedSuperclass), | ||
| 97 | $this->formatField('Embedded class?', $metadata->isEmbeddedClass), | ||
| 98 | $this->formatField('Parent classes', $metadata->parentClasses), | ||
| 99 | $this->formatField('Sub classes', $metadata->subClasses), | ||
| 100 | $this->formatField('Embedded classes', $metadata->subClasses), | ||
| 101 | $this->formatField('Identifier', $metadata->identifier), | ||
| 102 | $this->formatField('Inheritance type', $metadata->inheritanceType), | ||
| 103 | $this->formatField('Discriminator column', $metadata->discriminatorColumn), | ||
| 104 | $this->formatField('Discriminator value', $metadata->discriminatorValue), | ||
| 105 | $this->formatField('Discriminator map', $metadata->discriminatorMap), | ||
| 106 | $this->formatField('Generator type', $metadata->generatorType), | ||
| 107 | $this->formatField('Table', $metadata->table), | ||
| 108 | $this->formatField('Composite identifier?', $metadata->isIdentifierComposite), | ||
| 109 | $this->formatField('Foreign identifier?', $metadata->containsForeignIdentifier), | ||
| 110 | $this->formatField('Enum identifier?', $metadata->containsEnumIdentifier), | ||
| 111 | $this->formatField('Sequence generator definition', $metadata->sequenceGeneratorDefinition), | ||
| 112 | $this->formatField('Change tracking policy', $metadata->changeTrackingPolicy), | ||
| 113 | $this->formatField('Versioned?', $metadata->isVersioned), | ||
| 114 | $this->formatField('Version field', $metadata->versionField), | ||
| 115 | $this->formatField('Read only?', $metadata->isReadOnly), | ||
| 116 | |||
| 117 | $this->formatEntityListeners($metadata->entityListeners), | ||
| 118 | ], | ||
| 119 | [$this->formatField('Association mappings:', '')], | ||
| 120 | $this->formatMappings($metadata->associationMappings), | ||
| 121 | [$this->formatField('Field mappings:', '')], | ||
| 122 | $this->formatMappings($metadata->fieldMappings), | ||
| 123 | ), | ||
| 124 | ); | ||
| 125 | } | ||
| 126 | |||
| 127 | /** | ||
| 128 | * Return all mapped entity class names | ||
| 129 | * | ||
| 130 | * @return string[] | ||
| 131 | * @psalm-return class-string[] | ||
| 132 | */ | ||
| 133 | private function getMappedEntities(EntityManagerInterface $entityManager): array | ||
| 134 | { | ||
| 135 | $entityClassNames = $entityManager->getConfiguration() | ||
| 136 | ->getMetadataDriverImpl() | ||
| 137 | ->getAllClassNames(); | ||
| 138 | |||
| 139 | if (! $entityClassNames) { | ||
| 140 | throw new InvalidArgumentException( | ||
| 141 | 'You do not have any mapped Doctrine ORM entities according to the current configuration. ' . | ||
| 142 | 'If you have entities or mapping files you should check your mapping configuration for errors.', | ||
| 143 | ); | ||
| 144 | } | ||
| 145 | |||
| 146 | return $entityClassNames; | ||
| 147 | } | ||
| 148 | |||
| 149 | /** | ||
| 150 | * Return the class metadata for the given entity | ||
| 151 | * name | ||
| 152 | * | ||
| 153 | * @param string $entityName Full or partial entity name | ||
| 154 | */ | ||
| 155 | private function getClassMetadata( | ||
| 156 | string $entityName, | ||
| 157 | EntityManagerInterface $entityManager, | ||
| 158 | ): ClassMetadata { | ||
| 159 | try { | ||
| 160 | return $entityManager->getClassMetadata($entityName); | ||
| 161 | } catch (MappingException) { | ||
| 162 | } | ||
| 163 | |||
| 164 | $matches = array_filter( | ||
| 165 | $this->getMappedEntities($entityManager), | ||
| 166 | static fn ($mappedEntity) => preg_match('{' . preg_quote($entityName) . '}', $mappedEntity) | ||
| 167 | ); | ||
| 168 | |||
| 169 | if (! $matches) { | ||
| 170 | throw new InvalidArgumentException(sprintf( | ||
| 171 | 'Could not find any mapped Entity classes matching "%s"', | ||
| 172 | $entityName, | ||
| 173 | )); | ||
| 174 | } | ||
| 175 | |||
| 176 | if (count($matches) > 1) { | ||
| 177 | throw new InvalidArgumentException(sprintf( | ||
| 178 | 'Entity name "%s" is ambiguous, possible matches: "%s"', | ||
| 179 | $entityName, | ||
| 180 | implode(', ', $matches), | ||
| 181 | )); | ||
| 182 | } | ||
| 183 | |||
| 184 | return $entityManager->getClassMetadata(current($matches)); | ||
| 185 | } | ||
| 186 | |||
| 187 | /** | ||
| 188 | * Format the given value for console output | ||
| 189 | */ | ||
| 190 | private function formatValue(mixed $value): string | ||
| 191 | { | ||
| 192 | if ($value === '') { | ||
| 193 | return ''; | ||
| 194 | } | ||
| 195 | |||
| 196 | if ($value === null) { | ||
| 197 | return '<comment>Null</comment>'; | ||
| 198 | } | ||
| 199 | |||
| 200 | if (is_bool($value)) { | ||
| 201 | return '<comment>' . ($value ? 'True' : 'False') . '</comment>'; | ||
| 202 | } | ||
| 203 | |||
| 204 | if (empty($value)) { | ||
| 205 | return '<comment>Empty</comment>'; | ||
| 206 | } | ||
| 207 | |||
| 208 | if (is_array($value)) { | ||
| 209 | return json_encode( | ||
| 210 | $value, | ||
| 211 | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR, | ||
| 212 | ); | ||
| 213 | } | ||
| 214 | |||
| 215 | if (is_object($value)) { | ||
| 216 | return sprintf('<%s>', get_debug_type($value)); | ||
| 217 | } | ||
| 218 | |||
| 219 | if (is_scalar($value)) { | ||
| 220 | return (string) $value; | ||
| 221 | } | ||
| 222 | |||
| 223 | throw new InvalidArgumentException(sprintf('Do not know how to format value "%s"', print_r($value, true))); | ||
| 224 | } | ||
| 225 | |||
| 226 | /** | ||
| 227 | * Add the given label and value to the two column table output | ||
| 228 | * | ||
| 229 | * @param string $label Label for the value | ||
| 230 | * @param mixed $value A Value to show | ||
| 231 | * | ||
| 232 | * @return string[] | ||
| 233 | * @psalm-return array{0: string, 1: string} | ||
| 234 | */ | ||
| 235 | private function formatField(string $label, mixed $value): array | ||
| 236 | { | ||
| 237 | if ($value === null) { | ||
| 238 | $value = '<comment>None</comment>'; | ||
| 239 | } | ||
| 240 | |||
| 241 | return [sprintf('<info>%s</info>', $label), $this->formatValue($value)]; | ||
| 242 | } | ||
| 243 | |||
| 244 | /** | ||
| 245 | * Format the association mappings | ||
| 246 | * | ||
| 247 | * @psalm-param array<string, FieldMapping|AssociationMapping> $propertyMappings | ||
| 248 | * | ||
| 249 | * @return string[][] | ||
| 250 | * @psalm-return list<array{0: string, 1: string}> | ||
| 251 | */ | ||
| 252 | private function formatMappings(array $propertyMappings): array | ||
| 253 | { | ||
| 254 | $output = []; | ||
| 255 | |||
| 256 | foreach ($propertyMappings as $propertyName => $mapping) { | ||
| 257 | $output[] = $this->formatField(sprintf(' %s', $propertyName), ''); | ||
| 258 | |||
| 259 | foreach ((array) $mapping as $field => $value) { | ||
| 260 | $output[] = $this->formatField(sprintf(' %s', $field), $this->formatValue($value)); | ||
| 261 | } | ||
| 262 | } | ||
| 263 | |||
| 264 | return $output; | ||
| 265 | } | ||
| 266 | |||
| 267 | /** | ||
| 268 | * Format the entity listeners | ||
| 269 | * | ||
| 270 | * @psalm-param list<object> $entityListeners | ||
| 271 | * | ||
| 272 | * @return string[] | ||
| 273 | * @psalm-return array{0: string, 1: string} | ||
| 274 | */ | ||
| 275 | private function formatEntityListeners(array $entityListeners): array | ||
| 276 | { | ||
| 277 | return $this->formatField('Entity listeners', array_map('get_class', $entityListeners)); | ||
| 278 | } | ||
| 279 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Tools\Debug; | ||
| 8 | use LogicException; | ||
| 9 | use RuntimeException; | ||
| 10 | use Symfony\Component\Console\Input\InputArgument; | ||
| 11 | use Symfony\Component\Console\Input\InputInterface; | ||
| 12 | use Symfony\Component\Console\Input\InputOption; | ||
| 13 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 14 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 15 | |||
| 16 | use function constant; | ||
| 17 | use function defined; | ||
| 18 | use function is_numeric; | ||
| 19 | use function sprintf; | ||
| 20 | use function str_replace; | ||
| 21 | use function strtoupper; | ||
| 22 | |||
| 23 | /** | ||
| 24 | * Command to execute DQL queries in a given EntityManager. | ||
| 25 | * | ||
| 26 | * @link www.doctrine-project.org | ||
| 27 | */ | ||
| 28 | class RunDqlCommand extends AbstractEntityManagerCommand | ||
| 29 | { | ||
| 30 | protected function configure(): void | ||
| 31 | { | ||
| 32 | $this->setName('orm:run-dql') | ||
| 33 | ->setDescription('Executes arbitrary DQL directly from the command line') | ||
| 34 | ->addArgument('dql', InputArgument::REQUIRED, 'The DQL to execute.') | ||
| 35 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 36 | ->addOption('hydrate', null, InputOption::VALUE_REQUIRED, 'Hydration mode of result set. Should be either: object, array, scalar or single-scalar.', 'object') | ||
| 37 | ->addOption('first-result', null, InputOption::VALUE_REQUIRED, 'The first result in the result set.') | ||
| 38 | ->addOption('max-result', null, InputOption::VALUE_REQUIRED, 'The maximum number of results in the result set.') | ||
| 39 | ->addOption('depth', null, InputOption::VALUE_REQUIRED, 'Dumping depth of Entity graph.', 7) | ||
| 40 | ->addOption('show-sql', null, InputOption::VALUE_NONE, 'Dump generated SQL instead of executing query') | ||
| 41 | ->setHelp(<<<'EOT' | ||
| 42 | The <info>%command.name%</info> command executes the given DQL query and | ||
| 43 | outputs the results: | ||
| 44 | |||
| 45 | <info>php %command.full_name% "SELECT u FROM App\Entity\User u"</info> | ||
| 46 | |||
| 47 | You can also optionally specify some additional options like what type of | ||
| 48 | hydration to use when executing the query: | ||
| 49 | |||
| 50 | <info>php %command.full_name% "SELECT u FROM App\Entity\User u" --hydrate=array</info> | ||
| 51 | |||
| 52 | Additionally you can specify the first result and maximum amount of results to | ||
| 53 | show: | ||
| 54 | |||
| 55 | <info>php %command.full_name% "SELECT u FROM App\Entity\User u" --first-result=0 --max-result=30</info> | ||
| 56 | EOT); | ||
| 57 | } | ||
| 58 | |||
| 59 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 60 | { | ||
| 61 | $ui = new SymfonyStyle($input, $output); | ||
| 62 | |||
| 63 | $em = $this->getEntityManager($input); | ||
| 64 | |||
| 65 | $dql = $input->getArgument('dql'); | ||
| 66 | if ($dql === null) { | ||
| 67 | throw new RuntimeException("Argument 'dql' is required in order to execute this command correctly."); | ||
| 68 | } | ||
| 69 | |||
| 70 | $depth = $input->getOption('depth'); | ||
| 71 | |||
| 72 | if (! is_numeric($depth)) { | ||
| 73 | throw new LogicException("Option 'depth' must contain an integer value"); | ||
| 74 | } | ||
| 75 | |||
| 76 | $hydrationModeName = (string) $input->getOption('hydrate'); | ||
| 77 | $hydrationMode = 'Doctrine\ORM\Query::HYDRATE_' . strtoupper(str_replace('-', '_', $hydrationModeName)); | ||
| 78 | |||
| 79 | if (! defined($hydrationMode)) { | ||
| 80 | throw new RuntimeException(sprintf( | ||
| 81 | "Hydration mode '%s' does not exist. It should be either: object. array, scalar or single-scalar.", | ||
| 82 | $hydrationModeName, | ||
| 83 | )); | ||
| 84 | } | ||
| 85 | |||
| 86 | $query = $em->createQuery($dql); | ||
| 87 | |||
| 88 | $firstResult = $input->getOption('first-result'); | ||
| 89 | if ($firstResult !== null) { | ||
| 90 | if (! is_numeric($firstResult)) { | ||
| 91 | throw new LogicException("Option 'first-result' must contain an integer value"); | ||
| 92 | } | ||
| 93 | |||
| 94 | $query->setFirstResult((int) $firstResult); | ||
| 95 | } | ||
| 96 | |||
| 97 | $maxResult = $input->getOption('max-result'); | ||
| 98 | if ($maxResult !== null) { | ||
| 99 | if (! is_numeric($maxResult)) { | ||
| 100 | throw new LogicException("Option 'max-result' must contain an integer value"); | ||
| 101 | } | ||
| 102 | |||
| 103 | $query->setMaxResults((int) $maxResult); | ||
| 104 | } | ||
| 105 | |||
| 106 | if ($input->getOption('show-sql')) { | ||
| 107 | $ui->text($query->getSQL()); | ||
| 108 | |||
| 109 | return 0; | ||
| 110 | } | ||
| 111 | |||
| 112 | $resultSet = $query->execute([], constant($hydrationMode)); | ||
| 113 | |||
| 114 | $ui->text(Debug::dump($resultSet, (int) $input->getOption('depth'))); | ||
| 115 | |||
| 116 | return 0; | ||
| 117 | } | ||
| 118 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\SchemaTool; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand; | ||
| 8 | use Doctrine\ORM\Tools\SchemaTool; | ||
| 9 | use Symfony\Component\Console\Input\InputInterface; | ||
| 10 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 11 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 12 | |||
| 13 | /** | ||
| 14 | * Base class for CreateCommand, DropCommand and UpdateCommand. | ||
| 15 | * | ||
| 16 | * @link www.doctrine-project.org | ||
| 17 | */ | ||
| 18 | abstract class AbstractCommand extends AbstractEntityManagerCommand | ||
| 19 | { | ||
| 20 | /** @param mixed[] $metadatas */ | ||
| 21 | abstract protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui): int; | ||
| 22 | |||
| 23 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 24 | { | ||
| 25 | $ui = new SymfonyStyle($input, $output); | ||
| 26 | |||
| 27 | $em = $this->getEntityManager($input); | ||
| 28 | |||
| 29 | $metadatas = $em->getMetadataFactory()->getAllMetadata(); | ||
| 30 | |||
| 31 | if (empty($metadatas)) { | ||
| 32 | $ui->getErrorStyle()->success('No Metadata Classes to process.'); | ||
| 33 | |||
| 34 | return 0; | ||
| 35 | } | ||
| 36 | |||
| 37 | return $this->executeSchemaCommand($input, $output, new SchemaTool($em), $metadatas, $ui); | ||
| 38 | } | ||
| 39 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\SchemaTool; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Tools\SchemaTool; | ||
| 8 | use Symfony\Component\Console\Input\InputInterface; | ||
| 9 | use Symfony\Component\Console\Input\InputOption; | ||
| 10 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 11 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 12 | |||
| 13 | use function sprintf; | ||
| 14 | |||
| 15 | /** | ||
| 16 | * Command to create the database schema for a set of classes based on their mappings. | ||
| 17 | * | ||
| 18 | * @link www.doctrine-project.org | ||
| 19 | */ | ||
| 20 | class CreateCommand extends AbstractCommand | ||
| 21 | { | ||
| 22 | protected function configure(): void | ||
| 23 | { | ||
| 24 | $this->setName('orm:schema-tool:create') | ||
| 25 | ->setDescription('Processes the schema and either create it directly on EntityManager Storage Connection or generate the SQL output') | ||
| 26 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 27 | ->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Instead of trying to apply generated SQLs into EntityManager Storage Connection, output them.') | ||
| 28 | ->setHelp(<<<'EOT' | ||
| 29 | Processes the schema and either create it directly on EntityManager Storage Connection or generate the SQL output. | ||
| 30 | |||
| 31 | <comment>Hint:</comment> If you have a database with tables that should not be managed | ||
| 32 | by the ORM, you can use a DBAL functionality to filter the tables and sequences down | ||
| 33 | on a global level: | ||
| 34 | |||
| 35 | $config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool { | ||
| 36 | if ($assetName instanceof AbstractAsset) { | ||
| 37 | $assetName = $assetName->getName(); | ||
| 38 | } | ||
| 39 | |||
| 40 | return !str_starts_with($assetName, 'audit_'); | ||
| 41 | }); | ||
| 42 | EOT); | ||
| 43 | } | ||
| 44 | |||
| 45 | /** | ||
| 46 | * {@inheritDoc} | ||
| 47 | */ | ||
| 48 | protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui): int | ||
| 49 | { | ||
| 50 | $dumpSql = $input->getOption('dump-sql') === true; | ||
| 51 | |||
| 52 | if ($dumpSql) { | ||
| 53 | $sqls = $schemaTool->getCreateSchemaSql($metadatas); | ||
| 54 | |||
| 55 | foreach ($sqls as $sql) { | ||
| 56 | $ui->writeln(sprintf('%s;', $sql)); | ||
| 57 | } | ||
| 58 | |||
| 59 | return 0; | ||
| 60 | } | ||
| 61 | |||
| 62 | $notificationUi = $ui->getErrorStyle(); | ||
| 63 | |||
| 64 | $notificationUi->caution('This operation should not be executed in a production environment!'); | ||
| 65 | |||
| 66 | $notificationUi->text('Creating database schema...'); | ||
| 67 | $notificationUi->newLine(); | ||
| 68 | |||
| 69 | $schemaTool->createSchema($metadatas); | ||
| 70 | |||
| 71 | $notificationUi->success('Database schema created successfully!'); | ||
| 72 | |||
| 73 | return 0; | ||
| 74 | } | ||
| 75 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\SchemaTool; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Tools\SchemaTool; | ||
| 8 | use Symfony\Component\Console\Input\InputInterface; | ||
| 9 | use Symfony\Component\Console\Input\InputOption; | ||
| 10 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 11 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 12 | |||
| 13 | use function count; | ||
| 14 | use function sprintf; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Command to drop the database schema for a set of classes based on their mappings. | ||
| 18 | * | ||
| 19 | * @link www.doctrine-project.org | ||
| 20 | */ | ||
| 21 | class DropCommand extends AbstractCommand | ||
| 22 | { | ||
| 23 | protected function configure(): void | ||
| 24 | { | ||
| 25 | $this->setName('orm:schema-tool:drop') | ||
| 26 | ->setDescription('Drop the complete database schema of EntityManager Storage Connection or generate the corresponding SQL output') | ||
| 27 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 28 | ->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Instead of trying to apply generated SQLs into EntityManager Storage Connection, output them.') | ||
| 29 | ->addOption('force', 'f', InputOption::VALUE_NONE, "Don't ask for the deletion of the database, but force the operation to run.") | ||
| 30 | ->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.') | ||
| 31 | ->setHelp(<<<'EOT' | ||
| 32 | Processes the schema and either drop the database schema of EntityManager Storage Connection or generate the SQL output. | ||
| 33 | Beware that the complete database is dropped by this command, even tables that are not relevant to your metadata model. | ||
| 34 | |||
| 35 | <comment>Hint:</comment> If you have a database with tables that should not be managed | ||
| 36 | by the ORM, you can use a DBAL functionality to filter the tables and sequences down | ||
| 37 | on a global level: | ||
| 38 | |||
| 39 | $config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool { | ||
| 40 | if ($assetName instanceof AbstractAsset) { | ||
| 41 | $assetName = $assetName->getName(); | ||
| 42 | } | ||
| 43 | |||
| 44 | return !str_starts_with($assetName, 'audit_'); | ||
| 45 | }); | ||
| 46 | EOT); | ||
| 47 | } | ||
| 48 | |||
| 49 | /** | ||
| 50 | * {@inheritDoc} | ||
| 51 | */ | ||
| 52 | protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui): int | ||
| 53 | { | ||
| 54 | $isFullDatabaseDrop = $input->getOption('full-database'); | ||
| 55 | $dumpSql = $input->getOption('dump-sql') === true; | ||
| 56 | $force = $input->getOption('force') === true; | ||
| 57 | |||
| 58 | if ($dumpSql) { | ||
| 59 | if ($isFullDatabaseDrop) { | ||
| 60 | $sqls = $schemaTool->getDropDatabaseSQL(); | ||
| 61 | } else { | ||
| 62 | $sqls = $schemaTool->getDropSchemaSQL($metadatas); | ||
| 63 | } | ||
| 64 | |||
| 65 | foreach ($sqls as $sql) { | ||
| 66 | $ui->writeln(sprintf('%s;', $sql)); | ||
| 67 | } | ||
| 68 | |||
| 69 | return 0; | ||
| 70 | } | ||
| 71 | |||
| 72 | $notificationUi = $ui->getErrorStyle(); | ||
| 73 | |||
| 74 | if ($force) { | ||
| 75 | $notificationUi->text('Dropping database schema...'); | ||
| 76 | $notificationUi->newLine(); | ||
| 77 | |||
| 78 | if ($isFullDatabaseDrop) { | ||
| 79 | $schemaTool->dropDatabase(); | ||
| 80 | } else { | ||
| 81 | $schemaTool->dropSchema($metadatas); | ||
| 82 | } | ||
| 83 | |||
| 84 | $notificationUi->success('Database schema dropped successfully!'); | ||
| 85 | |||
| 86 | return 0; | ||
| 87 | } | ||
| 88 | |||
| 89 | $notificationUi->caution('This operation should not be executed in a production environment!'); | ||
| 90 | |||
| 91 | if ($isFullDatabaseDrop) { | ||
| 92 | $sqls = $schemaTool->getDropDatabaseSQL(); | ||
| 93 | } else { | ||
| 94 | $sqls = $schemaTool->getDropSchemaSQL($metadatas); | ||
| 95 | } | ||
| 96 | |||
| 97 | if (empty($sqls)) { | ||
| 98 | $notificationUi->success('Nothing to drop. The database is empty!'); | ||
| 99 | |||
| 100 | return 0; | ||
| 101 | } | ||
| 102 | |||
| 103 | $notificationUi->text( | ||
| 104 | [ | ||
| 105 | sprintf('The Schema-Tool would execute <info>"%s"</info> queries to update the database.', count($sqls)), | ||
| 106 | '', | ||
| 107 | 'Please run the operation by passing one - or both - of the following options:', | ||
| 108 | '', | ||
| 109 | sprintf(' <info>%s --force</info> to execute the command', $this->getName()), | ||
| 110 | sprintf(' <info>%s --dump-sql</info> to dump the SQL statements to the screen', $this->getName()), | ||
| 111 | ], | ||
| 112 | ); | ||
| 113 | |||
| 114 | return 1; | ||
| 115 | } | ||
| 116 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\SchemaTool; | ||
| 6 | |||
| 7 | use Doctrine\Deprecations\Deprecation; | ||
| 8 | use Doctrine\ORM\Tools\SchemaTool; | ||
| 9 | use Symfony\Component\Console\Input\InputInterface; | ||
| 10 | use Symfony\Component\Console\Input\InputOption; | ||
| 11 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 12 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 13 | |||
| 14 | use function count; | ||
| 15 | use function sprintf; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * Command to generate the SQL needed to update the database schema to match | ||
| 19 | * the current mapping information. | ||
| 20 | * | ||
| 21 | * @link www.doctrine-project.org | ||
| 22 | */ | ||
| 23 | class UpdateCommand extends AbstractCommand | ||
| 24 | { | ||
| 25 | protected string $name = 'orm:schema-tool:update'; | ||
| 26 | |||
| 27 | protected function configure(): void | ||
| 28 | { | ||
| 29 | $this->setName($this->name) | ||
| 30 | ->setDescription('Executes (or dumps) the SQL needed to update the database schema to match the current mapping metadata') | ||
| 31 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 32 | ->addOption('complete', null, InputOption::VALUE_NONE, 'This option is a no-op, is deprecated and will be removed in 4.0') | ||
| 33 | ->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Dumps the generated SQL statements to the screen (does not execute them).') | ||
| 34 | ->addOption('force', 'f', InputOption::VALUE_NONE, 'Causes the generated SQL statements to be physically executed against your database.') | ||
| 35 | ->setHelp(<<<'EOT' | ||
| 36 | The <info>%command.name%</info> command generates the SQL needed to | ||
| 37 | synchronize the database schema with the current mapping metadata of the | ||
| 38 | default entity manager. | ||
| 39 | |||
| 40 | For example, if you add metadata for a new column to an entity, this command | ||
| 41 | would generate and output the SQL needed to add the new column to the database: | ||
| 42 | |||
| 43 | <info>%command.name% --dump-sql</info> | ||
| 44 | |||
| 45 | Alternatively, you can execute the generated queries: | ||
| 46 | |||
| 47 | <info>%command.name% --force</info> | ||
| 48 | |||
| 49 | If both options are specified, the queries are output and then executed: | ||
| 50 | |||
| 51 | <info>%command.name% --dump-sql --force</info> | ||
| 52 | |||
| 53 | Finally, be aware that this task will drop all database assets (e.g. tables, | ||
| 54 | etc) that are *not* described by the current metadata. In other words, without | ||
| 55 | this option, this task leaves untouched any "extra" tables that exist in the | ||
| 56 | database, but which aren't described by any metadata. | ||
| 57 | |||
| 58 | <comment>Hint:</comment> If you have a database with tables that should not be managed | ||
| 59 | by the ORM, you can use a DBAL functionality to filter the tables and sequences down | ||
| 60 | on a global level: | ||
| 61 | |||
| 62 | $config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool { | ||
| 63 | if ($assetName instanceof AbstractAsset) { | ||
| 64 | $assetName = $assetName->getName(); | ||
| 65 | } | ||
| 66 | |||
| 67 | return !str_starts_with($assetName, 'audit_'); | ||
| 68 | }); | ||
| 69 | EOT); | ||
| 70 | } | ||
| 71 | |||
| 72 | /** | ||
| 73 | * {@inheritDoc} | ||
| 74 | */ | ||
| 75 | protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui): int | ||
| 76 | { | ||
| 77 | $notificationUi = $ui->getErrorStyle(); | ||
| 78 | |||
| 79 | if ($input->getOption('complete') === true) { | ||
| 80 | Deprecation::trigger( | ||
| 81 | 'doctrine/orm', | ||
| 82 | 'https://github.com/doctrine/orm/pull/11354', | ||
| 83 | 'The --complete option is a no-op, is deprecated and will be removed in Doctrine ORM 4.0.', | ||
| 84 | ); | ||
| 85 | $notificationUi->warning('The --complete option is a no-op, is deprecated and will be removed in Doctrine ORM 4.0.'); | ||
| 86 | } | ||
| 87 | |||
| 88 | $sqls = $schemaTool->getUpdateSchemaSql($metadatas); | ||
| 89 | |||
| 90 | if (empty($sqls)) { | ||
| 91 | $notificationUi->success('Nothing to update - your database is already in sync with the current entity metadata.'); | ||
| 92 | |||
| 93 | return 0; | ||
| 94 | } | ||
| 95 | |||
| 96 | $dumpSql = $input->getOption('dump-sql') === true; | ||
| 97 | $force = $input->getOption('force') === true; | ||
| 98 | |||
| 99 | if ($dumpSql) { | ||
| 100 | foreach ($sqls as $sql) { | ||
| 101 | $ui->writeln(sprintf('%s;', $sql)); | ||
| 102 | } | ||
| 103 | } | ||
| 104 | |||
| 105 | if ($force) { | ||
| 106 | if ($dumpSql) { | ||
| 107 | $notificationUi->newLine(); | ||
| 108 | } | ||
| 109 | |||
| 110 | $notificationUi->text('Updating database schema...'); | ||
| 111 | $notificationUi->newLine(); | ||
| 112 | |||
| 113 | $schemaTool->updateSchema($metadatas); | ||
| 114 | |||
| 115 | $pluralization = count($sqls) === 1 ? 'query was' : 'queries were'; | ||
| 116 | |||
| 117 | $notificationUi->text(sprintf(' <info>%s</info> %s executed', count($sqls), $pluralization)); | ||
| 118 | $notificationUi->success('Database schema updated successfully!'); | ||
| 119 | } | ||
| 120 | |||
| 121 | if ($dumpSql || $force) { | ||
| 122 | return 0; | ||
| 123 | } | ||
| 124 | |||
| 125 | $notificationUi->caution( | ||
| 126 | [ | ||
| 127 | 'This operation should not be executed in a production environment!', | ||
| 128 | '', | ||
| 129 | 'Use the incremental update to detect changes during development and use', | ||
| 130 | 'the SQL DDL provided to manually update your database in production.', | ||
| 131 | ], | ||
| 132 | ); | ||
| 133 | |||
| 134 | $notificationUi->text( | ||
| 135 | [ | ||
| 136 | sprintf('The Schema-Tool would execute <info>"%s"</info> queries to update the database.', count($sqls)), | ||
| 137 | '', | ||
| 138 | 'Please run the operation by passing one - or both - of the following options:', | ||
| 139 | '', | ||
| 140 | sprintf(' <info>%s --force</info> to execute the command', $this->getName()), | ||
| 141 | sprintf(' <info>%s --dump-sql</info> to dump the SQL statements to the screen', $this->getName()), | ||
| 142 | ], | ||
| 143 | ); | ||
| 144 | |||
| 145 | return 1; | ||
| 146 | } | ||
| 147 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Tools\SchemaValidator; | ||
| 8 | use Symfony\Component\Console\Input\InputInterface; | ||
| 9 | use Symfony\Component\Console\Input\InputOption; | ||
| 10 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 11 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 12 | |||
| 13 | use function count; | ||
| 14 | use function sprintf; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Command to validate that the current mapping is valid. | ||
| 18 | * | ||
| 19 | * @link www.doctrine-project.com | ||
| 20 | */ | ||
| 21 | class ValidateSchemaCommand extends AbstractEntityManagerCommand | ||
| 22 | { | ||
| 23 | protected function configure(): void | ||
| 24 | { | ||
| 25 | $this->setName('orm:validate-schema') | ||
| 26 | ->setDescription('Validate the mapping files') | ||
| 27 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 28 | ->addOption('skip-mapping', null, InputOption::VALUE_NONE, 'Skip the mapping validation check') | ||
| 29 | ->addOption('skip-sync', null, InputOption::VALUE_NONE, 'Skip checking if the mapping is in sync with the database') | ||
| 30 | ->addOption('skip-property-types', null, InputOption::VALUE_NONE, 'Skip checking if property types match the Doctrine types') | ||
| 31 | ->setHelp('Validate that the mapping files are correct and in sync with the database.'); | ||
| 32 | } | ||
| 33 | |||
| 34 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 35 | { | ||
| 36 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 37 | |||
| 38 | $em = $this->getEntityManager($input); | ||
| 39 | $validator = new SchemaValidator($em, ! $input->getOption('skip-property-types')); | ||
| 40 | $exit = 0; | ||
| 41 | |||
| 42 | $ui->section('Mapping'); | ||
| 43 | |||
| 44 | if ($input->getOption('skip-mapping')) { | ||
| 45 | $ui->text('<comment>[SKIPPED] The mapping was not checked.</comment>'); | ||
| 46 | } else { | ||
| 47 | $errors = $validator->validateMapping(); | ||
| 48 | if ($errors) { | ||
| 49 | foreach ($errors as $className => $errorMessages) { | ||
| 50 | $ui->text( | ||
| 51 | sprintf( | ||
| 52 | '<error>[FAIL]</error> The entity-class <comment>%s</comment> mapping is invalid:', | ||
| 53 | $className, | ||
| 54 | ), | ||
| 55 | ); | ||
| 56 | |||
| 57 | $ui->listing($errorMessages); | ||
| 58 | $ui->newLine(); | ||
| 59 | } | ||
| 60 | |||
| 61 | ++$exit; | ||
| 62 | } else { | ||
| 63 | $ui->success('The mapping files are correct.'); | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | $ui->section('Database'); | ||
| 68 | |||
| 69 | if ($input->getOption('skip-sync')) { | ||
| 70 | $ui->text('<comment>[SKIPPED] The database was not checked for synchronicity.</comment>'); | ||
| 71 | } elseif (! $validator->schemaInSyncWithMetadata()) { | ||
| 72 | $ui->error('The database schema is not in sync with the current mapping file.'); | ||
| 73 | |||
| 74 | if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { | ||
| 75 | $sqls = $validator->getUpdateSchemaList(); | ||
| 76 | $ui->comment(sprintf('<info>%d</info> schema diff(s) detected:', count($sqls))); | ||
| 77 | foreach ($sqls as $sql) { | ||
| 78 | $ui->text(sprintf(' %s;', $sql)); | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | $exit += 2; | ||
| 83 | } else { | ||
| 84 | $ui->success('The database schema is in sync with the mapping files.'); | ||
| 85 | } | ||
| 86 | |||
| 87 | return $exit; | ||
| 88 | } | ||
| 89 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console; | ||
| 6 | |||
| 7 | use Composer\InstalledVersions; | ||
| 8 | use Doctrine\DBAL\Tools\Console as DBALConsole; | ||
| 9 | use Doctrine\ORM\Tools\Console\EntityManagerProvider\ConnectionFromManagerProvider; | ||
| 10 | use OutOfBoundsException; | ||
| 11 | use Symfony\Component\Console\Application; | ||
| 12 | use Symfony\Component\Console\Command\Command as SymfonyCommand; | ||
| 13 | |||
| 14 | use function assert; | ||
| 15 | use function class_exists; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * Handles running the Console Tools inside Symfony Console context. | ||
| 19 | */ | ||
| 20 | final class ConsoleRunner | ||
| 21 | { | ||
| 22 | /** | ||
| 23 | * Runs console with the given helper set. | ||
| 24 | * | ||
| 25 | * @param SymfonyCommand[] $commands | ||
| 26 | */ | ||
| 27 | public static function run(EntityManagerProvider $entityManagerProvider, array $commands = []): void | ||
| 28 | { | ||
| 29 | $cli = self::createApplication($entityManagerProvider, $commands); | ||
| 30 | $cli->run(); | ||
| 31 | } | ||
| 32 | |||
| 33 | /** | ||
| 34 | * Creates a console application with the given helperset and | ||
| 35 | * optional commands. | ||
| 36 | * | ||
| 37 | * @param SymfonyCommand[] $commands | ||
| 38 | * | ||
| 39 | * @throws OutOfBoundsException | ||
| 40 | */ | ||
| 41 | public static function createApplication( | ||
| 42 | EntityManagerProvider $entityManagerProvider, | ||
| 43 | array $commands = [], | ||
| 44 | ): Application { | ||
| 45 | $version = InstalledVersions::getVersion('doctrine/orm'); | ||
| 46 | assert($version !== null); | ||
| 47 | |||
| 48 | $cli = new Application('Doctrine Command Line Interface', $version); | ||
| 49 | $cli->setCatchExceptions(true); | ||
| 50 | |||
| 51 | self::addCommands($cli, $entityManagerProvider); | ||
| 52 | $cli->addCommands($commands); | ||
| 53 | |||
| 54 | return $cli; | ||
| 55 | } | ||
| 56 | |||
| 57 | public static function addCommands(Application $cli, EntityManagerProvider $entityManagerProvider): void | ||
| 58 | { | ||
| 59 | $connectionProvider = new ConnectionFromManagerProvider($entityManagerProvider); | ||
| 60 | |||
| 61 | if (class_exists(DBALConsole\Command\ReservedWordsCommand::class)) { | ||
| 62 | $cli->add(new DBALConsole\Command\ReservedWordsCommand($connectionProvider)); | ||
| 63 | } | ||
| 64 | |||
| 65 | $cli->addCommands( | ||
| 66 | [ | ||
| 67 | // DBAL Commands | ||
| 68 | new DBALConsole\Command\RunSqlCommand($connectionProvider), | ||
| 69 | |||
| 70 | // ORM Commands | ||
| 71 | new Command\ClearCache\CollectionRegionCommand($entityManagerProvider), | ||
| 72 | new Command\ClearCache\EntityRegionCommand($entityManagerProvider), | ||
| 73 | new Command\ClearCache\MetadataCommand($entityManagerProvider), | ||
| 74 | new Command\ClearCache\QueryCommand($entityManagerProvider), | ||
| 75 | new Command\ClearCache\QueryRegionCommand($entityManagerProvider), | ||
| 76 | new Command\ClearCache\ResultCommand($entityManagerProvider), | ||
| 77 | new Command\SchemaTool\CreateCommand($entityManagerProvider), | ||
| 78 | new Command\SchemaTool\UpdateCommand($entityManagerProvider), | ||
| 79 | new Command\SchemaTool\DropCommand($entityManagerProvider), | ||
| 80 | new Command\GenerateProxiesCommand($entityManagerProvider), | ||
| 81 | new Command\RunDqlCommand($entityManagerProvider), | ||
| 82 | new Command\ValidateSchemaCommand($entityManagerProvider), | ||
| 83 | new Command\InfoCommand($entityManagerProvider), | ||
| 84 | new Command\MappingDescribeCommand($entityManagerProvider), | ||
| 85 | ], | ||
| 86 | ); | ||
| 87 | } | ||
| 88 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | |||
| 9 | interface EntityManagerProvider | ||
| 10 | { | ||
| 11 | public function getDefaultManager(): EntityManagerInterface; | ||
| 12 | |||
| 13 | public function getManager(string $name): EntityManagerInterface; | ||
| 14 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/ConnectionFromManagerProvider.php b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/ConnectionFromManagerProvider.php new file mode 100644 index 0000000..0776601 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/ConnectionFromManagerProvider.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\EntityManagerProvider; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Connection; | ||
| 8 | use Doctrine\DBAL\Tools\Console\ConnectionProvider; | ||
| 9 | use Doctrine\ORM\Tools\Console\EntityManagerProvider; | ||
| 10 | |||
| 11 | final class ConnectionFromManagerProvider implements ConnectionProvider | ||
| 12 | { | ||
| 13 | public function __construct(private readonly EntityManagerProvider $entityManagerProvider) | ||
| 14 | { | ||
| 15 | } | ||
| 16 | |||
| 17 | public function getDefaultConnection(): Connection | ||
| 18 | { | ||
| 19 | return $this->entityManagerProvider->getDefaultManager()->getConnection(); | ||
| 20 | } | ||
| 21 | |||
| 22 | public function getConnection(string $name): Connection | ||
| 23 | { | ||
| 24 | return $this->entityManagerProvider->getManager($name)->getConnection(); | ||
| 25 | } | ||
| 26 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\EntityManagerProvider; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\ORM\Tools\Console\EntityManagerProvider; | ||
| 9 | |||
| 10 | final class SingleManagerProvider implements EntityManagerProvider | ||
| 11 | { | ||
| 12 | public function __construct( | ||
| 13 | private readonly EntityManagerInterface $entityManager, | ||
| 14 | private readonly string $defaultManagerName = 'default', | ||
| 15 | ) { | ||
| 16 | } | ||
| 17 | |||
| 18 | public function getDefaultManager(): EntityManagerInterface | ||
| 19 | { | ||
| 20 | return $this->entityManager; | ||
| 21 | } | ||
| 22 | |||
| 23 | public function getManager(string $name): EntityManagerInterface | ||
| 24 | { | ||
| 25 | if ($name !== $this->defaultManagerName) { | ||
| 26 | throw UnknownManagerException::unknownManager($name, [$this->defaultManagerName]); | ||
| 27 | } | ||
| 28 | |||
| 29 | return $this->entityManager; | ||
| 30 | } | ||
| 31 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\EntityManagerProvider; | ||
| 6 | |||
| 7 | use OutOfBoundsException; | ||
| 8 | |||
| 9 | use function implode; | ||
| 10 | use function sprintf; | ||
| 11 | |||
| 12 | final class UnknownManagerException extends OutOfBoundsException | ||
| 13 | { | ||
| 14 | /** @psalm-param list<string> $knownManagers */ | ||
| 15 | public static function unknownManager(string $unknownManager, array $knownManagers = []): self | ||
| 16 | { | ||
| 17 | return new self(sprintf( | ||
| 18 | 'Requested unknown entity manager: %s, known managers: %s', | ||
| 19 | $unknownManager, | ||
| 20 | implode(', ', $knownManagers), | ||
| 21 | )); | ||
| 22 | } | ||
| 23 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console; | ||
| 6 | |||
| 7 | use ArrayIterator; | ||
| 8 | use Countable; | ||
| 9 | use Doctrine\Persistence\Mapping\ClassMetadata; | ||
| 10 | use FilterIterator; | ||
| 11 | use RuntimeException; | ||
| 12 | |||
| 13 | use function assert; | ||
| 14 | use function count; | ||
| 15 | use function iterator_to_array; | ||
| 16 | use function preg_match; | ||
| 17 | use function sprintf; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Used by CLI Tools to restrict entity-based commands to given patterns. | ||
| 21 | * | ||
| 22 | * @link www.doctrine-project.com | ||
| 23 | */ | ||
| 24 | class MetadataFilter extends FilterIterator implements Countable | ||
| 25 | { | ||
| 26 | /** @var mixed[] */ | ||
| 27 | private array $filter = []; | ||
| 28 | |||
| 29 | /** | ||
| 30 | * Filter Metadatas by one or more filter options. | ||
| 31 | * | ||
| 32 | * @param ClassMetadata[] $metadatas | ||
| 33 | * @param string[]|string $filter | ||
| 34 | * | ||
| 35 | * @return ClassMetadata[] | ||
| 36 | */ | ||
| 37 | public static function filter(array $metadatas, array|string $filter): array | ||
| 38 | { | ||
| 39 | $metadatas = new MetadataFilter(new ArrayIterator($metadatas), $filter); | ||
| 40 | |||
| 41 | return iterator_to_array($metadatas); | ||
| 42 | } | ||
| 43 | |||
| 44 | /** @param mixed[]|string $filter */ | ||
| 45 | public function __construct(ArrayIterator $metadata, array|string $filter) | ||
| 46 | { | ||
| 47 | $this->filter = (array) $filter; | ||
| 48 | |||
| 49 | parent::__construct($metadata); | ||
| 50 | } | ||
| 51 | |||
| 52 | public function accept(): bool | ||
| 53 | { | ||
| 54 | if (count($this->filter) === 0) { | ||
| 55 | return true; | ||
| 56 | } | ||
| 57 | |||
| 58 | $it = $this->getInnerIterator(); | ||
| 59 | $metadata = $it->current(); | ||
| 60 | |||
| 61 | foreach ($this->filter as $filter) { | ||
| 62 | $pregResult = preg_match('/' . $filter . '/', $metadata->getName()); | ||
| 63 | |||
| 64 | if ($pregResult === false) { | ||
| 65 | throw new RuntimeException( | ||
| 66 | sprintf("Error while evaluating regex '/%s/'.", $filter), | ||
| 67 | ); | ||
| 68 | } | ||
| 69 | |||
| 70 | if ($pregResult) { | ||
| 71 | return true; | ||
| 72 | } | ||
| 73 | } | ||
| 74 | |||
| 75 | return false; | ||
| 76 | } | ||
| 77 | |||
| 78 | /** @return ArrayIterator<int, ClassMetadata> */ | ||
| 79 | public function getInnerIterator(): ArrayIterator | ||
| 80 | { | ||
| 81 | $innerIterator = parent::getInnerIterator(); | ||
| 82 | |||
| 83 | assert($innerIterator instanceof ArrayIterator); | ||
| 84 | |||
| 85 | return $innerIterator; | ||
| 86 | } | ||
| 87 | |||
| 88 | public function count(): int | ||
| 89 | { | ||
| 90 | return count($this->getInnerIterator()); | ||
| 91 | } | ||
| 92 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools; | ||
| 6 | |||
| 7 | use ArrayIterator; | ||
| 8 | use ArrayObject; | ||
| 9 | use DateTimeInterface; | ||
| 10 | use Doctrine\Common\Collections\Collection; | ||
| 11 | use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; | ||
| 12 | use Doctrine\Persistence\Proxy; | ||
| 13 | use stdClass; | ||
| 14 | |||
| 15 | use function array_keys; | ||
| 16 | use function count; | ||
| 17 | use function end; | ||
| 18 | use function explode; | ||
| 19 | use function extension_loaded; | ||
| 20 | use function html_entity_decode; | ||
| 21 | use function ini_get; | ||
| 22 | use function ini_set; | ||
| 23 | use function is_array; | ||
| 24 | use function is_object; | ||
| 25 | use function ob_end_clean; | ||
| 26 | use function ob_get_contents; | ||
| 27 | use function ob_start; | ||
| 28 | use function strip_tags; | ||
| 29 | use function var_dump; | ||
| 30 | |||
| 31 | /** | ||
| 32 | * Static class containing most used debug methods. | ||
| 33 | * | ||
| 34 | * @internal | ||
| 35 | * | ||
| 36 | * @link www.doctrine-project.org | ||
| 37 | */ | ||
| 38 | final class Debug | ||
| 39 | { | ||
| 40 | /** | ||
| 41 | * Private constructor (prevents instantiation). | ||
| 42 | */ | ||
| 43 | private function __construct() | ||
| 44 | { | ||
| 45 | } | ||
| 46 | |||
| 47 | /** | ||
| 48 | * Prints a dump of the public, protected and private properties of $var. | ||
| 49 | * | ||
| 50 | * @link https://xdebug.org/ | ||
| 51 | * | ||
| 52 | * @param mixed $var The variable to dump. | ||
| 53 | * @param int $maxDepth The maximum nesting level for object properties. | ||
| 54 | */ | ||
| 55 | public static function dump(mixed $var, int $maxDepth = 2): string | ||
| 56 | { | ||
| 57 | $html = ini_get('html_errors'); | ||
| 58 | |||
| 59 | if ($html !== '1') { | ||
| 60 | ini_set('html_errors', 'on'); | ||
| 61 | } | ||
| 62 | |||
| 63 | if (extension_loaded('xdebug')) { | ||
| 64 | $previousDepth = ini_get('xdebug.var_display_max_depth'); | ||
| 65 | ini_set('xdebug.var_display_max_depth', (string) $maxDepth); | ||
| 66 | } | ||
| 67 | |||
| 68 | try { | ||
| 69 | $var = self::export($var, $maxDepth); | ||
| 70 | |||
| 71 | ob_start(); | ||
| 72 | var_dump($var); | ||
| 73 | |||
| 74 | $dump = ob_get_contents(); | ||
| 75 | |||
| 76 | ob_end_clean(); | ||
| 77 | |||
| 78 | $dumpText = strip_tags(html_entity_decode($dump)); | ||
| 79 | } finally { | ||
| 80 | ini_set('html_errors', $html); | ||
| 81 | |||
| 82 | if (isset($previousDepth)) { | ||
| 83 | ini_set('xdebug.var_display_max_depth', $previousDepth); | ||
| 84 | } | ||
| 85 | } | ||
| 86 | |||
| 87 | return $dumpText; | ||
| 88 | } | ||
| 89 | |||
| 90 | public static function export(mixed $var, int $maxDepth): mixed | ||
| 91 | { | ||
| 92 | if ($var instanceof Collection) { | ||
| 93 | $var = $var->toArray(); | ||
| 94 | } | ||
| 95 | |||
| 96 | if (! $maxDepth) { | ||
| 97 | return is_object($var) ? $var::class | ||
| 98 | : (is_array($var) ? 'Array(' . count($var) . ')' : $var); | ||
| 99 | } | ||
| 100 | |||
| 101 | if (is_array($var)) { | ||
| 102 | $return = []; | ||
| 103 | |||
| 104 | foreach ($var as $k => $v) { | ||
| 105 | $return[$k] = self::export($v, $maxDepth - 1); | ||
| 106 | } | ||
| 107 | |||
| 108 | return $return; | ||
| 109 | } | ||
| 110 | |||
| 111 | if (! is_object($var)) { | ||
| 112 | return $var; | ||
| 113 | } | ||
| 114 | |||
| 115 | $return = new stdClass(); | ||
| 116 | if ($var instanceof DateTimeInterface) { | ||
| 117 | $return->__CLASS__ = $var::class; | ||
| 118 | $return->date = $var->format('c'); | ||
| 119 | $return->timezone = $var->getTimezone()->getName(); | ||
| 120 | |||
| 121 | return $return; | ||
| 122 | } | ||
| 123 | |||
| 124 | $return->__CLASS__ = DefaultProxyClassNameResolver::getClass($var); | ||
| 125 | |||
| 126 | if ($var instanceof Proxy) { | ||
| 127 | $return->__IS_PROXY__ = true; | ||
| 128 | $return->__PROXY_INITIALIZED__ = $var->__isInitialized(); | ||
| 129 | } | ||
| 130 | |||
| 131 | if ($var instanceof ArrayObject || $var instanceof ArrayIterator) { | ||
| 132 | $return->__STORAGE__ = self::export($var->getArrayCopy(), $maxDepth - 1); | ||
| 133 | } | ||
| 134 | |||
| 135 | return self::fillReturnWithClassAttributes($var, $return, $maxDepth); | ||
| 136 | } | ||
| 137 | |||
| 138 | /** | ||
| 139 | * Fill the $return variable with class attributes | ||
| 140 | * Based on obj2array function from {@see https://secure.php.net/manual/en/function.get-object-vars.php#47075} | ||
| 141 | */ | ||
| 142 | private static function fillReturnWithClassAttributes(object $var, stdClass $return, int $maxDepth): stdClass | ||
| 143 | { | ||
| 144 | $clone = (array) $var; | ||
| 145 | |||
| 146 | foreach (array_keys($clone) as $key) { | ||
| 147 | $aux = explode("\0", (string) $key); | ||
| 148 | $name = end($aux); | ||
| 149 | if ($aux[0] === '') { | ||
| 150 | $name .= ':' . ($aux[1] === '*' ? 'protected' : $aux[1] . ':private'); | ||
| 151 | } | ||
| 152 | |||
| 153 | $return->$name = self::export($clone[$key], $maxDepth - 1); | ||
| 154 | } | ||
| 155 | |||
| 156 | return $return; | ||
| 157 | } | ||
| 158 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\ORM\Event\OnFlushEventArgs; | ||
| 9 | use Doctrine\ORM\PersistentCollection; | ||
| 10 | use Doctrine\ORM\UnitOfWork; | ||
| 11 | use ReflectionObject; | ||
| 12 | |||
| 13 | use function count; | ||
| 14 | use function fclose; | ||
| 15 | use function fopen; | ||
| 16 | use function fwrite; | ||
| 17 | use function gettype; | ||
| 18 | use function is_object; | ||
| 19 | use function spl_object_id; | ||
| 20 | |||
| 21 | /** | ||
| 22 | * Use this logger to dump the identity map during the onFlush event. This is useful for debugging | ||
| 23 | * weird UnitOfWork behavior with complex operations. | ||
| 24 | */ | ||
| 25 | class DebugUnitOfWorkListener | ||
| 26 | { | ||
| 27 | /** | ||
| 28 | * Pass a stream and context information for the debugging session. | ||
| 29 | * | ||
| 30 | * The stream can be php://output to print to the screen. | ||
| 31 | */ | ||
| 32 | public function __construct( | ||
| 33 | private readonly string $file = 'php://output', | ||
| 34 | private readonly string $context = '', | ||
| 35 | ) { | ||
| 36 | } | ||
| 37 | |||
| 38 | public function onFlush(OnFlushEventArgs $args): void | ||
| 39 | { | ||
| 40 | $this->dumpIdentityMap($args->getObjectManager()); | ||
| 41 | } | ||
| 42 | |||
| 43 | /** | ||
| 44 | * Dumps the contents of the identity map into a stream. | ||
| 45 | */ | ||
| 46 | public function dumpIdentityMap(EntityManagerInterface $em): void | ||
| 47 | { | ||
| 48 | $uow = $em->getUnitOfWork(); | ||
| 49 | $identityMap = $uow->getIdentityMap(); | ||
| 50 | |||
| 51 | $fh = fopen($this->file, 'xb+'); | ||
| 52 | if (count($identityMap) === 0) { | ||
| 53 | fwrite($fh, 'Flush Operation [' . $this->context . "] - Empty identity map.\n"); | ||
| 54 | |||
| 55 | return; | ||
| 56 | } | ||
| 57 | |||
| 58 | fwrite($fh, 'Flush Operation [' . $this->context . "] - Dumping identity map:\n"); | ||
| 59 | foreach ($identityMap as $className => $map) { | ||
| 60 | fwrite($fh, 'Class: ' . $className . "\n"); | ||
| 61 | |||
| 62 | foreach ($map as $entity) { | ||
| 63 | fwrite($fh, ' Entity: ' . $this->getIdString($entity, $uow) . ' ' . spl_object_id($entity) . "\n"); | ||
| 64 | fwrite($fh, " Associations:\n"); | ||
| 65 | |||
| 66 | $cm = $em->getClassMetadata($className); | ||
| 67 | |||
| 68 | foreach ($cm->associationMappings as $field => $assoc) { | ||
| 69 | fwrite($fh, ' ' . $field . ' '); | ||
| 70 | $value = $cm->getFieldValue($entity, $field); | ||
| 71 | |||
| 72 | if ($assoc->isToOne()) { | ||
| 73 | if ($value === null) { | ||
| 74 | fwrite($fh, " NULL\n"); | ||
| 75 | } else { | ||
| 76 | if ($uow->isUninitializedObject($value)) { | ||
| 77 | fwrite($fh, '[PROXY] '); | ||
| 78 | } | ||
| 79 | |||
| 80 | fwrite($fh, $this->getIdString($value, $uow) . ' ' . spl_object_id($value) . "\n"); | ||
| 81 | } | ||
| 82 | } else { | ||
| 83 | $initialized = ! ($value instanceof PersistentCollection) || $value->isInitialized(); | ||
| 84 | if ($value === null) { | ||
| 85 | fwrite($fh, " NULL\n"); | ||
| 86 | } elseif ($initialized) { | ||
| 87 | fwrite($fh, '[INITIALIZED] ' . $this->getType($value) . ' ' . count($value) . " elements\n"); | ||
| 88 | |||
| 89 | foreach ($value as $obj) { | ||
| 90 | fwrite($fh, ' ' . $this->getIdString($obj, $uow) . ' ' . spl_object_id($obj) . "\n"); | ||
| 91 | } | ||
| 92 | } else { | ||
| 93 | fwrite($fh, '[PROXY] ' . $this->getType($value) . " unknown element size\n"); | ||
| 94 | foreach ($value->unwrap() as $obj) { | ||
| 95 | fwrite($fh, ' ' . $this->getIdString($obj, $uow) . ' ' . spl_object_id($obj) . "\n"); | ||
| 96 | } | ||
| 97 | } | ||
| 98 | } | ||
| 99 | } | ||
| 100 | } | ||
| 101 | } | ||
| 102 | |||
| 103 | fclose($fh); | ||
| 104 | } | ||
| 105 | |||
| 106 | private function getType(mixed $var): string | ||
| 107 | { | ||
| 108 | if (is_object($var)) { | ||
| 109 | $refl = new ReflectionObject($var); | ||
| 110 | |||
| 111 | return $refl->getShortName(); | ||
| 112 | } | ||
| 113 | |||
| 114 | return gettype($var); | ||
| 115 | } | ||
| 116 | |||
| 117 | private function getIdString(object $entity, UnitOfWork $uow): string | ||
| 118 | { | ||
| 119 | if ($uow->isInIdentityMap($entity)) { | ||
| 120 | $ids = $uow->getEntityIdentifier($entity); | ||
| 121 | $idstring = ''; | ||
| 122 | |||
| 123 | foreach ($ids as $k => $v) { | ||
| 124 | $idstring .= $k . '=' . $v; | ||
| 125 | } | ||
| 126 | } else { | ||
| 127 | $idstring = 'NEWOBJECT '; | ||
| 128 | } | ||
| 129 | |||
| 130 | $state = $uow->getEntityState($entity); | ||
| 131 | |||
| 132 | if ($state === UnitOfWork::STATE_NEW) { | ||
| 133 | $idstring .= ' [NEW]'; | ||
| 134 | } elseif ($state === UnitOfWork::STATE_REMOVED) { | ||
| 135 | $idstring .= ' [REMOVED]'; | ||
| 136 | } elseif ($state === UnitOfWork::STATE_MANAGED) { | ||
| 137 | $idstring .= ' [MANAGED]'; | ||
| 138 | } elseif ($state === UnitOfWork::STATE_DETACHED) { | ||
| 139 | $idstring .= ' [DETACHED]'; | ||
| 140 | } | ||
| 141 | |||
| 142 | return $idstring; | ||
| 143 | } | ||
| 144 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Event; | ||
| 6 | |||
| 7 | use Doctrine\Common\EventArgs; | ||
| 8 | use Doctrine\DBAL\Schema\Schema; | ||
| 9 | use Doctrine\ORM\EntityManagerInterface; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * Event Args used for the Events::postGenerateSchema event. | ||
| 13 | * | ||
| 14 | * @link www.doctrine-project.com | ||
| 15 | */ | ||
| 16 | class GenerateSchemaEventArgs extends EventArgs | ||
| 17 | { | ||
| 18 | public function __construct( | ||
| 19 | private readonly EntityManagerInterface $em, | ||
| 20 | private readonly Schema $schema, | ||
| 21 | ) { | ||
| 22 | } | ||
| 23 | |||
| 24 | public function getEntityManager(): EntityManagerInterface | ||
| 25 | { | ||
| 26 | return $this->em; | ||
| 27 | } | ||
| 28 | |||
| 29 | public function getSchema(): Schema | ||
| 30 | { | ||
| 31 | return $this->schema; | ||
| 32 | } | ||
| 33 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Event; | ||
| 6 | |||
| 7 | use Doctrine\Common\EventArgs; | ||
| 8 | use Doctrine\DBAL\Schema\Schema; | ||
| 9 | use Doctrine\DBAL\Schema\Table; | ||
| 10 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * Event Args used for the Events::postGenerateSchemaTable event. | ||
| 14 | * | ||
| 15 | * @link www.doctrine-project.com | ||
| 16 | */ | ||
| 17 | class GenerateSchemaTableEventArgs extends EventArgs | ||
| 18 | { | ||
| 19 | public function __construct( | ||
| 20 | private readonly ClassMetadata $classMetadata, | ||
| 21 | private readonly Schema $schema, | ||
| 22 | private readonly Table $classTable, | ||
| 23 | ) { | ||
| 24 | } | ||
| 25 | |||
| 26 | public function getClassMetadata(): ClassMetadata | ||
| 27 | { | ||
| 28 | return $this->classMetadata; | ||
| 29 | } | ||
| 30 | |||
| 31 | public function getSchema(): Schema | ||
| 32 | { | ||
| 33 | return $this->schema; | ||
| 34 | } | ||
| 35 | |||
| 36 | public function getClassTable(): Table | ||
| 37 | { | ||
| 38 | return $this->classTable; | ||
| 39 | } | ||
| 40 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\ORMException; | ||
| 8 | use LogicException; | ||
| 9 | |||
| 10 | use function sprintf; | ||
| 11 | |||
| 12 | final class MissingColumnException extends LogicException implements ORMException | ||
| 13 | { | ||
| 14 | public static function fromColumnSourceAndTarget(string $column, string $source, string $target): self | ||
| 15 | { | ||
| 16 | return new self(sprintf( | ||
| 17 | 'Column name "%s" referenced for relation from %s towards %s does not exist.', | ||
| 18 | $column, | ||
| 19 | $source, | ||
| 20 | $target, | ||
| 21 | )); | ||
| 22 | } | ||
| 23 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Exception/NotSupported.php b/vendor/doctrine/orm/src/Tools/Exception/NotSupported.php new file mode 100644 index 0000000..af619fd --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Exception/NotSupported.php | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\SchemaToolException; | ||
| 8 | use LogicException; | ||
| 9 | |||
| 10 | final class NotSupported extends LogicException implements SchemaToolException | ||
| 11 | { | ||
| 12 | public static function create(): self | ||
| 13 | { | ||
| 14 | return new self('This behaviour is (currently) not supported by Doctrine 2'); | ||
| 15 | } | ||
| 16 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/CountOutputWalker.php b/vendor/doctrine/orm/src/Tools/Pagination/CountOutputWalker.php new file mode 100644 index 0000000..c7f31db --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Pagination/CountOutputWalker.php | |||
| @@ -0,0 +1,125 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Pagination; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 8 | use Doctrine\DBAL\Platforms\SQLServerPlatform; | ||
| 9 | use Doctrine\ORM\Query; | ||
| 10 | use Doctrine\ORM\Query\AST\SelectStatement; | ||
| 11 | use Doctrine\ORM\Query\Parser; | ||
| 12 | use Doctrine\ORM\Query\ParserResult; | ||
| 13 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 14 | use Doctrine\ORM\Query\SqlWalker; | ||
| 15 | use RuntimeException; | ||
| 16 | |||
| 17 | use function array_diff; | ||
| 18 | use function array_keys; | ||
| 19 | use function assert; | ||
| 20 | use function count; | ||
| 21 | use function implode; | ||
| 22 | use function reset; | ||
| 23 | use function sprintf; | ||
| 24 | |||
| 25 | /** | ||
| 26 | * Wraps the query in order to accurately count the root objects. | ||
| 27 | * | ||
| 28 | * Given a DQL like `SELECT u FROM User u` it will generate an SQL query like: | ||
| 29 | * SELECT COUNT(*) (SELECT DISTINCT <id> FROM (<original SQL>)) | ||
| 30 | * | ||
| 31 | * Works with composite keys but cannot deal with queries that have multiple | ||
| 32 | * root entities (e.g. `SELECT f, b from Foo, Bar`) | ||
| 33 | * | ||
| 34 | * Note that the ORDER BY clause is not removed. Many SQL implementations (e.g. MySQL) | ||
| 35 | * are able to cache subqueries. By keeping the ORDER BY clause intact, the limitSubQuery | ||
| 36 | * that will most likely be executed next can be read from the native SQL cache. | ||
| 37 | * | ||
| 38 | * @psalm-import-type QueryComponent from Parser | ||
| 39 | */ | ||
| 40 | class CountOutputWalker extends SqlWalker | ||
| 41 | { | ||
| 42 | private readonly AbstractPlatform $platform; | ||
| 43 | private readonly ResultSetMapping $rsm; | ||
| 44 | |||
| 45 | /** | ||
| 46 | * {@inheritDoc} | ||
| 47 | */ | ||
| 48 | public function __construct(Query $query, ParserResult $parserResult, array $queryComponents) | ||
| 49 | { | ||
| 50 | $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform(); | ||
| 51 | $this->rsm = $parserResult->getResultSetMapping(); | ||
| 52 | |||
| 53 | parent::__construct($query, $parserResult, $queryComponents); | ||
| 54 | } | ||
| 55 | |||
| 56 | public function walkSelectStatement(SelectStatement $selectStatement): string | ||
| 57 | { | ||
| 58 | if ($this->platform instanceof SQLServerPlatform) { | ||
| 59 | $selectStatement->orderByClause = null; | ||
| 60 | } | ||
| 61 | |||
| 62 | $sql = parent::walkSelectStatement($selectStatement); | ||
| 63 | |||
| 64 | if ($selectStatement->groupByClause) { | ||
| 65 | return sprintf( | ||
| 66 | 'SELECT COUNT(*) AS dctrn_count FROM (%s) dctrn_table', | ||
| 67 | $sql, | ||
| 68 | ); | ||
| 69 | } | ||
| 70 | |||
| 71 | // Find out the SQL alias of the identifier column of the root entity | ||
| 72 | // It may be possible to make this work with multiple root entities but that | ||
| 73 | // would probably require issuing multiple queries or doing a UNION SELECT | ||
| 74 | // so for now, It's not supported. | ||
| 75 | |||
| 76 | // Get the root entity and alias from the AST fromClause | ||
| 77 | $from = $selectStatement->fromClause->identificationVariableDeclarations; | ||
| 78 | if (count($from) > 1) { | ||
| 79 | throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction'); | ||
| 80 | } | ||
| 81 | |||
| 82 | $fromRoot = reset($from); | ||
| 83 | $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; | ||
| 84 | $rootClass = $this->getMetadataForDqlAlias($rootAlias); | ||
| 85 | $rootIdentifier = $rootClass->identifier; | ||
| 86 | |||
| 87 | // For every identifier, find out the SQL alias by combing through the ResultSetMapping | ||
| 88 | $sqlIdentifier = []; | ||
| 89 | foreach ($rootIdentifier as $property) { | ||
| 90 | if (isset($rootClass->fieldMappings[$property])) { | ||
| 91 | foreach (array_keys($this->rsm->fieldMappings, $property, true) as $alias) { | ||
| 92 | if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) { | ||
| 93 | $sqlIdentifier[$property] = $alias; | ||
| 94 | } | ||
| 95 | } | ||
| 96 | } | ||
| 97 | |||
| 98 | if (isset($rootClass->associationMappings[$property])) { | ||
| 99 | $association = $rootClass->associationMappings[$property]; | ||
| 100 | assert($association->isToOneOwningSide()); | ||
| 101 | $joinColumn = $association->joinColumns[0]->name; | ||
| 102 | |||
| 103 | foreach (array_keys($this->rsm->metaMappings, $joinColumn, true) as $alias) { | ||
| 104 | if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) { | ||
| 105 | $sqlIdentifier[$property] = $alias; | ||
| 106 | } | ||
| 107 | } | ||
| 108 | } | ||
| 109 | } | ||
| 110 | |||
| 111 | if (count($rootIdentifier) !== count($sqlIdentifier)) { | ||
| 112 | throw new RuntimeException(sprintf( | ||
| 113 | 'Not all identifier properties can be found in the ResultSetMapping: %s', | ||
| 114 | implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier))), | ||
| 115 | )); | ||
| 116 | } | ||
| 117 | |||
| 118 | // Build the counter query | ||
| 119 | return sprintf( | ||
| 120 | 'SELECT COUNT(*) AS dctrn_count FROM (SELECT DISTINCT %s FROM (%s) dctrn_result) dctrn_table', | ||
| 121 | implode(', ', $sqlIdentifier), | ||
| 122 | $sql, | ||
| 123 | ); | ||
| 124 | } | ||
| 125 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Pagination; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\AggregateExpression; | ||
| 8 | use Doctrine\ORM\Query\AST\PathExpression; | ||
| 9 | use Doctrine\ORM\Query\AST\SelectExpression; | ||
| 10 | use Doctrine\ORM\Query\AST\SelectStatement; | ||
| 11 | use Doctrine\ORM\Query\TreeWalkerAdapter; | ||
| 12 | use RuntimeException; | ||
| 13 | |||
| 14 | use function count; | ||
| 15 | use function reset; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * Replaces the selectClause of the AST with a COUNT statement. | ||
| 19 | */ | ||
| 20 | class CountWalker extends TreeWalkerAdapter | ||
| 21 | { | ||
| 22 | /** | ||
| 23 | * Distinct mode hint name. | ||
| 24 | */ | ||
| 25 | public const HINT_DISTINCT = 'doctrine_paginator.distinct'; | ||
| 26 | |||
| 27 | public function walkSelectStatement(SelectStatement $selectStatement): void | ||
| 28 | { | ||
| 29 | if ($selectStatement->havingClause) { | ||
| 30 | throw new RuntimeException('Cannot count query that uses a HAVING clause. Use the output walkers for pagination'); | ||
| 31 | } | ||
| 32 | |||
| 33 | // Get the root entity and alias from the AST fromClause | ||
| 34 | $from = $selectStatement->fromClause->identificationVariableDeclarations; | ||
| 35 | |||
| 36 | if (count($from) > 1) { | ||
| 37 | throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction'); | ||
| 38 | } | ||
| 39 | |||
| 40 | $fromRoot = reset($from); | ||
| 41 | $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; | ||
| 42 | $rootClass = $this->getMetadataForDqlAlias($rootAlias); | ||
| 43 | $identifierFieldName = $rootClass->getSingleIdentifierFieldName(); | ||
| 44 | |||
| 45 | $pathType = PathExpression::TYPE_STATE_FIELD; | ||
| 46 | if (isset($rootClass->associationMappings[$identifierFieldName])) { | ||
| 47 | $pathType = PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; | ||
| 48 | } | ||
| 49 | |||
| 50 | $pathExpression = new PathExpression( | ||
| 51 | PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, | ||
| 52 | $rootAlias, | ||
| 53 | $identifierFieldName, | ||
| 54 | ); | ||
| 55 | $pathExpression->type = $pathType; | ||
| 56 | |||
| 57 | $distinct = $this->_getQuery()->getHint(self::HINT_DISTINCT); | ||
| 58 | $selectStatement->selectClause->selectExpressions = [ | ||
| 59 | new SelectExpression( | ||
| 60 | new AggregateExpression('count', $pathExpression, $distinct), | ||
| 61 | null, | ||
| 62 | ), | ||
| 63 | ]; | ||
| 64 | |||
| 65 | // ORDER BY is not needed, only increases query execution through unnecessary sorting. | ||
| 66 | $selectStatement->orderByClause = null; | ||
| 67 | } | ||
| 68 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Pagination\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\ORMException; | ||
| 8 | use LogicException; | ||
| 9 | |||
| 10 | final class RowNumberOverFunctionNotEnabled extends LogicException implements ORMException | ||
| 11 | { | ||
| 12 | public static function create(): self | ||
| 13 | { | ||
| 14 | return new self('The RowNumberOverFunction is not intended for, nor is it enabled for use in DQL.'); | ||
| 15 | } | ||
| 16 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryOutputWalker.php b/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryOutputWalker.php new file mode 100644 index 0000000..8bbc44c --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryOutputWalker.php | |||
| @@ -0,0 +1,544 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Pagination; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 8 | use Doctrine\DBAL\Platforms\DB2Platform; | ||
| 9 | use Doctrine\DBAL\Platforms\OraclePlatform; | ||
| 10 | use Doctrine\DBAL\Platforms\PostgreSQLPlatform; | ||
| 11 | use Doctrine\DBAL\Platforms\SQLServerPlatform; | ||
| 12 | use Doctrine\ORM\EntityManagerInterface; | ||
| 13 | use Doctrine\ORM\Mapping\QuoteStrategy; | ||
| 14 | use Doctrine\ORM\OptimisticLockException; | ||
| 15 | use Doctrine\ORM\Query; | ||
| 16 | use Doctrine\ORM\Query\AST\OrderByClause; | ||
| 17 | use Doctrine\ORM\Query\AST\PathExpression; | ||
| 18 | use Doctrine\ORM\Query\AST\SelectExpression; | ||
| 19 | use Doctrine\ORM\Query\AST\SelectStatement; | ||
| 20 | use Doctrine\ORM\Query\AST\Subselect; | ||
| 21 | use Doctrine\ORM\Query\Parser; | ||
| 22 | use Doctrine\ORM\Query\ParserResult; | ||
| 23 | use Doctrine\ORM\Query\QueryException; | ||
| 24 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 25 | use Doctrine\ORM\Query\SqlWalker; | ||
| 26 | use RuntimeException; | ||
| 27 | |||
| 28 | use function array_diff; | ||
| 29 | use function array_keys; | ||
| 30 | use function assert; | ||
| 31 | use function count; | ||
| 32 | use function implode; | ||
| 33 | use function in_array; | ||
| 34 | use function is_string; | ||
| 35 | use function method_exists; | ||
| 36 | use function preg_replace; | ||
| 37 | use function reset; | ||
| 38 | use function sprintf; | ||
| 39 | use function strrpos; | ||
| 40 | use function substr; | ||
| 41 | |||
| 42 | /** | ||
| 43 | * Wraps the query in order to select root entity IDs for pagination. | ||
| 44 | * | ||
| 45 | * Given a DQL like `SELECT u FROM User u` it will generate an SQL query like: | ||
| 46 | * SELECT DISTINCT <id> FROM (<original SQL>) LIMIT x OFFSET y | ||
| 47 | * | ||
| 48 | * Works with composite keys but cannot deal with queries that have multiple | ||
| 49 | * root entities (e.g. `SELECT f, b from Foo, Bar`) | ||
| 50 | * | ||
| 51 | * @psalm-import-type QueryComponent from Parser | ||
| 52 | */ | ||
| 53 | class LimitSubqueryOutputWalker extends SqlWalker | ||
| 54 | { | ||
| 55 | private const ORDER_BY_PATH_EXPRESSION = '/(?<![a-z0-9_])%s\.%s(?![a-z0-9_])/i'; | ||
| 56 | |||
| 57 | private readonly AbstractPlatform $platform; | ||
| 58 | private readonly ResultSetMapping $rsm; | ||
| 59 | private readonly int $firstResult; | ||
| 60 | private readonly int|null $maxResults; | ||
| 61 | private readonly EntityManagerInterface $em; | ||
| 62 | private readonly QuoteStrategy $quoteStrategy; | ||
| 63 | |||
| 64 | /** @var list<PathExpression> */ | ||
| 65 | private array $orderByPathExpressions = []; | ||
| 66 | |||
| 67 | /** | ||
| 68 | * We don't want to add path expressions from sub-selects into the select clause of the containing query. | ||
| 69 | * This state flag simply keeps track on whether we are walking on a subquery or not | ||
| 70 | */ | ||
| 71 | private bool $inSubSelect = false; | ||
| 72 | |||
| 73 | /** | ||
| 74 | * Stores various parameters that are otherwise unavailable | ||
| 75 | * because Doctrine\ORM\Query\SqlWalker keeps everything private without | ||
| 76 | * accessors. | ||
| 77 | * | ||
| 78 | * {@inheritDoc} | ||
| 79 | */ | ||
| 80 | public function __construct( | ||
| 81 | Query $query, | ||
| 82 | ParserResult $parserResult, | ||
| 83 | array $queryComponents, | ||
| 84 | ) { | ||
| 85 | $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform(); | ||
| 86 | $this->rsm = $parserResult->getResultSetMapping(); | ||
| 87 | |||
| 88 | // Reset limit and offset | ||
| 89 | $this->firstResult = $query->getFirstResult(); | ||
| 90 | $this->maxResults = $query->getMaxResults(); | ||
| 91 | $query->setFirstResult(0)->setMaxResults(null); | ||
| 92 | |||
| 93 | $this->em = $query->getEntityManager(); | ||
| 94 | $this->quoteStrategy = $this->em->getConfiguration()->getQuoteStrategy(); | ||
| 95 | |||
| 96 | parent::__construct($query, $parserResult, $queryComponents); | ||
| 97 | } | ||
| 98 | |||
| 99 | /** | ||
| 100 | * Check if the platform supports the ROW_NUMBER window function. | ||
| 101 | */ | ||
| 102 | private function platformSupportsRowNumber(): bool | ||
| 103 | { | ||
| 104 | return $this->platform instanceof PostgreSQLPlatform | ||
| 105 | || $this->platform instanceof SQLServerPlatform | ||
| 106 | || $this->platform instanceof OraclePlatform | ||
| 107 | || $this->platform instanceof DB2Platform | ||
| 108 | || (method_exists($this->platform, 'supportsRowNumberFunction') | ||
| 109 | && $this->platform->supportsRowNumberFunction()); | ||
| 110 | } | ||
| 111 | |||
| 112 | /** | ||
| 113 | * Rebuilds a select statement's order by clause for use in a | ||
| 114 | * ROW_NUMBER() OVER() expression. | ||
| 115 | */ | ||
| 116 | private function rebuildOrderByForRowNumber(SelectStatement $AST): void | ||
| 117 | { | ||
| 118 | $orderByClause = $AST->orderByClause; | ||
| 119 | $selectAliasToExpressionMap = []; | ||
| 120 | // Get any aliases that are available for select expressions. | ||
| 121 | foreach ($AST->selectClause->selectExpressions as $selectExpression) { | ||
| 122 | $selectAliasToExpressionMap[$selectExpression->fieldIdentificationVariable] = $selectExpression->expression; | ||
| 123 | } | ||
| 124 | |||
| 125 | // Rebuild string orderby expressions to use the select expression they're referencing | ||
| 126 | foreach ($orderByClause->orderByItems as $orderByItem) { | ||
| 127 | if (is_string($orderByItem->expression) && isset($selectAliasToExpressionMap[$orderByItem->expression])) { | ||
| 128 | $orderByItem->expression = $selectAliasToExpressionMap[$orderByItem->expression]; | ||
| 129 | } | ||
| 130 | } | ||
| 131 | |||
| 132 | $func = new RowNumberOverFunction('dctrn_rownum'); | ||
| 133 | $func->orderByClause = $AST->orderByClause; | ||
| 134 | $AST->selectClause->selectExpressions[] = new SelectExpression($func, 'dctrn_rownum', true); | ||
| 135 | |||
| 136 | // No need for an order by clause, we'll order by rownum in the outer query. | ||
| 137 | $AST->orderByClause = null; | ||
| 138 | } | ||
| 139 | |||
| 140 | public function walkSelectStatement(SelectStatement $selectStatement): string | ||
| 141 | { | ||
| 142 | if ($this->platformSupportsRowNumber()) { | ||
| 143 | return $this->walkSelectStatementWithRowNumber($selectStatement); | ||
| 144 | } | ||
| 145 | |||
| 146 | return $this->walkSelectStatementWithoutRowNumber($selectStatement); | ||
| 147 | } | ||
| 148 | |||
| 149 | /** | ||
| 150 | * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT. | ||
| 151 | * This method is for use with platforms which support ROW_NUMBER. | ||
| 152 | * | ||
| 153 | * @throws RuntimeException | ||
| 154 | */ | ||
| 155 | public function walkSelectStatementWithRowNumber(SelectStatement $AST): string | ||
| 156 | { | ||
| 157 | $hasOrderBy = false; | ||
| 158 | $outerOrderBy = ' ORDER BY dctrn_minrownum ASC'; | ||
| 159 | $orderGroupBy = ''; | ||
| 160 | if ($AST->orderByClause instanceof OrderByClause) { | ||
| 161 | $hasOrderBy = true; | ||
| 162 | $this->rebuildOrderByForRowNumber($AST); | ||
| 163 | } | ||
| 164 | |||
| 165 | $innerSql = $this->getInnerSQL($AST); | ||
| 166 | |||
| 167 | $sqlIdentifier = $this->getSQLIdentifier($AST); | ||
| 168 | |||
| 169 | if ($hasOrderBy) { | ||
| 170 | $orderGroupBy = ' GROUP BY ' . implode(', ', $sqlIdentifier); | ||
| 171 | $sqlIdentifier[] = 'MIN(' . $this->walkResultVariable('dctrn_rownum') . ') AS dctrn_minrownum'; | ||
| 172 | } | ||
| 173 | |||
| 174 | // Build the counter query | ||
| 175 | $sql = sprintf( | ||
| 176 | 'SELECT DISTINCT %s FROM (%s) dctrn_result', | ||
| 177 | implode(', ', $sqlIdentifier), | ||
| 178 | $innerSql, | ||
| 179 | ); | ||
| 180 | |||
| 181 | if ($hasOrderBy) { | ||
| 182 | $sql .= $orderGroupBy . $outerOrderBy; | ||
| 183 | } | ||
| 184 | |||
| 185 | // Apply the limit and offset. | ||
| 186 | $sql = $this->platform->modifyLimitQuery( | ||
| 187 | $sql, | ||
| 188 | $this->maxResults, | ||
| 189 | $this->firstResult, | ||
| 190 | ); | ||
| 191 | |||
| 192 | // Add the columns to the ResultSetMapping. It's not really nice but | ||
| 193 | // it works. Preferably I'd clear the RSM or simply create a new one | ||
| 194 | // but that is not possible from inside the output walker, so we dirty | ||
| 195 | // up the one we have. | ||
| 196 | foreach ($sqlIdentifier as $property => $alias) { | ||
| 197 | $this->rsm->addScalarResult($alias, $property); | ||
| 198 | } | ||
| 199 | |||
| 200 | return $sql; | ||
| 201 | } | ||
| 202 | |||
| 203 | /** | ||
| 204 | * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT. | ||
| 205 | * This method is for platforms which DO NOT support ROW_NUMBER. | ||
| 206 | * | ||
| 207 | * @throws RuntimeException | ||
| 208 | */ | ||
| 209 | public function walkSelectStatementWithoutRowNumber(SelectStatement $AST, bool $addMissingItemsFromOrderByToSelect = true): string | ||
| 210 | { | ||
| 211 | // We don't want to call this recursively! | ||
| 212 | if ($AST->orderByClause instanceof OrderByClause && $addMissingItemsFromOrderByToSelect) { | ||
| 213 | // In the case of ordering a query by columns from joined tables, we | ||
| 214 | // must add those columns to the select clause of the query BEFORE | ||
| 215 | // the SQL is generated. | ||
| 216 | $this->addMissingItemsFromOrderByToSelect($AST); | ||
| 217 | } | ||
| 218 | |||
| 219 | // Remove order by clause from the inner query | ||
| 220 | // It will be re-appended in the outer select generated by this method | ||
| 221 | $orderByClause = $AST->orderByClause; | ||
| 222 | $AST->orderByClause = null; | ||
| 223 | |||
| 224 | $innerSql = $this->getInnerSQL($AST); | ||
| 225 | |||
| 226 | $sqlIdentifier = $this->getSQLIdentifier($AST); | ||
| 227 | |||
| 228 | // Build the counter query | ||
| 229 | $sql = sprintf( | ||
| 230 | 'SELECT DISTINCT %s FROM (%s) dctrn_result', | ||
| 231 | implode(', ', $sqlIdentifier), | ||
| 232 | $innerSql, | ||
| 233 | ); | ||
| 234 | |||
| 235 | // https://github.com/doctrine/orm/issues/2630 | ||
| 236 | $sql = $this->preserveSqlOrdering($sqlIdentifier, $innerSql, $sql, $orderByClause); | ||
| 237 | |||
| 238 | // Apply the limit and offset. | ||
| 239 | $sql = $this->platform->modifyLimitQuery( | ||
| 240 | $sql, | ||
| 241 | $this->maxResults, | ||
| 242 | $this->firstResult, | ||
| 243 | ); | ||
| 244 | |||
| 245 | // Add the columns to the ResultSetMapping. It's not really nice but | ||
| 246 | // it works. Preferably I'd clear the RSM or simply create a new one | ||
| 247 | // but that is not possible from inside the output walker, so we dirty | ||
| 248 | // up the one we have. | ||
| 249 | foreach ($sqlIdentifier as $property => $alias) { | ||
| 250 | $this->rsm->addScalarResult($alias, $property); | ||
| 251 | } | ||
| 252 | |||
| 253 | // Restore orderByClause | ||
| 254 | $AST->orderByClause = $orderByClause; | ||
| 255 | |||
| 256 | return $sql; | ||
| 257 | } | ||
| 258 | |||
| 259 | /** | ||
| 260 | * Finds all PathExpressions in an AST's OrderByClause, and ensures that | ||
| 261 | * the referenced fields are present in the SelectClause of the passed AST. | ||
| 262 | */ | ||
| 263 | private function addMissingItemsFromOrderByToSelect(SelectStatement $AST): void | ||
| 264 | { | ||
| 265 | $this->orderByPathExpressions = []; | ||
| 266 | |||
| 267 | // We need to do this in another walker because otherwise we'll end up | ||
| 268 | // polluting the state of this one. | ||
| 269 | $walker = clone $this; | ||
| 270 | |||
| 271 | // This will populate $orderByPathExpressions via | ||
| 272 | // LimitSubqueryOutputWalker::walkPathExpression, which will be called | ||
| 273 | // as the select statement is walked. We'll end up with an array of all | ||
| 274 | // path expressions referenced in the query. | ||
| 275 | $walker->walkSelectStatementWithoutRowNumber($AST, false); | ||
| 276 | $orderByPathExpressions = $walker->getOrderByPathExpressions(); | ||
| 277 | |||
| 278 | // Get a map of referenced identifiers to field names. | ||
| 279 | $selects = []; | ||
| 280 | foreach ($orderByPathExpressions as $pathExpression) { | ||
| 281 | assert($pathExpression->field !== null); | ||
| 282 | $idVar = $pathExpression->identificationVariable; | ||
| 283 | $field = $pathExpression->field; | ||
| 284 | if (! isset($selects[$idVar])) { | ||
| 285 | $selects[$idVar] = []; | ||
| 286 | } | ||
| 287 | |||
| 288 | $selects[$idVar][$field] = true; | ||
| 289 | } | ||
| 290 | |||
| 291 | // Loop the select clause of the AST and exclude items from $select | ||
| 292 | // that are already being selected in the query. | ||
| 293 | foreach ($AST->selectClause->selectExpressions as $selectExpression) { | ||
| 294 | if ($selectExpression instanceof SelectExpression) { | ||
| 295 | $idVar = $selectExpression->expression; | ||
| 296 | if (! is_string($idVar)) { | ||
| 297 | continue; | ||
| 298 | } | ||
| 299 | |||
| 300 | $field = $selectExpression->fieldIdentificationVariable; | ||
| 301 | if ($field === null) { | ||
| 302 | // No need to add this select, as we're already fetching the whole object. | ||
| 303 | unset($selects[$idVar]); | ||
| 304 | } else { | ||
| 305 | unset($selects[$idVar][$field]); | ||
| 306 | } | ||
| 307 | } | ||
| 308 | } | ||
| 309 | |||
| 310 | // Add select items which were not excluded to the AST's select clause. | ||
| 311 | foreach ($selects as $idVar => $fields) { | ||
| 312 | $AST->selectClause->selectExpressions[] = new SelectExpression($idVar, null, true); | ||
| 313 | } | ||
| 314 | } | ||
| 315 | |||
| 316 | /** | ||
| 317 | * Generates new SQL for statements with an order by clause | ||
| 318 | * | ||
| 319 | * @param mixed[] $sqlIdentifier | ||
| 320 | */ | ||
| 321 | private function preserveSqlOrdering( | ||
| 322 | array $sqlIdentifier, | ||
| 323 | string $innerSql, | ||
| 324 | string $sql, | ||
| 325 | OrderByClause|null $orderByClause, | ||
| 326 | ): string { | ||
| 327 | // If the sql statement has an order by clause, we need to wrap it in a new select distinct statement | ||
| 328 | if (! $orderByClause) { | ||
| 329 | return $sql; | ||
| 330 | } | ||
| 331 | |||
| 332 | // now only select distinct identifier | ||
| 333 | return sprintf( | ||
| 334 | 'SELECT DISTINCT %s FROM (%s) dctrn_result', | ||
| 335 | implode(', ', $sqlIdentifier), | ||
| 336 | $this->recreateInnerSql($orderByClause, $sqlIdentifier, $innerSql), | ||
| 337 | ); | ||
| 338 | } | ||
| 339 | |||
| 340 | /** | ||
| 341 | * Generates a new SQL statement for the inner query to keep the correct sorting | ||
| 342 | * | ||
| 343 | * @param mixed[] $identifiers | ||
| 344 | */ | ||
| 345 | private function recreateInnerSql( | ||
| 346 | OrderByClause $orderByClause, | ||
| 347 | array $identifiers, | ||
| 348 | string $innerSql, | ||
| 349 | ): string { | ||
| 350 | [$searchPatterns, $replacements] = $this->generateSqlAliasReplacements(); | ||
| 351 | $orderByItems = []; | ||
| 352 | |||
| 353 | foreach ($orderByClause->orderByItems as $orderByItem) { | ||
| 354 | // Walk order by item to get string representation of it and | ||
| 355 | // replace path expressions in the order by clause with their column alias | ||
| 356 | $orderByItemString = preg_replace( | ||
| 357 | $searchPatterns, | ||
| 358 | $replacements, | ||
| 359 | $this->walkOrderByItem($orderByItem), | ||
| 360 | ); | ||
| 361 | |||
| 362 | $orderByItems[] = $orderByItemString; | ||
| 363 | $identifier = substr($orderByItemString, 0, strrpos($orderByItemString, ' ')); | ||
| 364 | |||
| 365 | if (! in_array($identifier, $identifiers, true)) { | ||
| 366 | $identifiers[] = $identifier; | ||
| 367 | } | ||
| 368 | } | ||
| 369 | |||
| 370 | return $sql = sprintf( | ||
| 371 | 'SELECT DISTINCT %s FROM (%s) dctrn_result_inner ORDER BY %s', | ||
| 372 | implode(', ', $identifiers), | ||
| 373 | $innerSql, | ||
| 374 | implode(', ', $orderByItems), | ||
| 375 | ); | ||
| 376 | } | ||
| 377 | |||
| 378 | /** | ||
| 379 | * @return string[][] | ||
| 380 | * @psalm-return array{0: list<non-empty-string>, 1: list<string>} | ||
| 381 | */ | ||
| 382 | private function generateSqlAliasReplacements(): array | ||
| 383 | { | ||
| 384 | $aliasMap = $searchPatterns = $replacements = $metadataList = []; | ||
| 385 | |||
| 386 | // Generate DQL alias -> SQL table alias mapping | ||
| 387 | foreach (array_keys($this->rsm->aliasMap) as $dqlAlias) { | ||
| 388 | $metadataList[$dqlAlias] = $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
| 389 | $aliasMap[$dqlAlias] = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); | ||
| 390 | } | ||
| 391 | |||
| 392 | // Generate search patterns for each field's path expression in the order by clause | ||
| 393 | foreach ($this->rsm->fieldMappings as $fieldAlias => $fieldName) { | ||
| 394 | $dqlAliasForFieldAlias = $this->rsm->columnOwnerMap[$fieldAlias]; | ||
| 395 | $class = $metadataList[$dqlAliasForFieldAlias]; | ||
| 396 | |||
| 397 | // If the field is from a joined child table, we won't be ordering on it. | ||
| 398 | if (! isset($class->fieldMappings[$fieldName])) { | ||
| 399 | continue; | ||
| 400 | } | ||
| 401 | |||
| 402 | $fieldMapping = $class->fieldMappings[$fieldName]; | ||
| 403 | |||
| 404 | // Get the proper column name as will appear in the select list | ||
| 405 | $columnName = $this->quoteStrategy->getColumnName( | ||
| 406 | $fieldName, | ||
| 407 | $metadataList[$dqlAliasForFieldAlias], | ||
| 408 | $this->em->getConnection()->getDatabasePlatform(), | ||
| 409 | ); | ||
| 410 | |||
| 411 | // Get the SQL table alias for the entity and field | ||
| 412 | $sqlTableAliasForFieldAlias = $aliasMap[$dqlAliasForFieldAlias]; | ||
| 413 | |||
| 414 | if (isset($fieldMapping->declared) && $fieldMapping->declared !== $class->name) { | ||
| 415 | // Field was declared in a parent class, so we need to get the proper SQL table alias | ||
| 416 | // for the joined parent table. | ||
| 417 | $otherClassMetadata = $this->em->getClassMetadata($fieldMapping->declared); | ||
| 418 | |||
| 419 | if (! $otherClassMetadata->isMappedSuperclass) { | ||
| 420 | $sqlTableAliasForFieldAlias = $this->getSQLTableAlias($otherClassMetadata->getTableName(), $dqlAliasForFieldAlias); | ||
| 421 | } | ||
| 422 | } | ||
| 423 | |||
| 424 | // Compose search and replace patterns | ||
| 425 | $searchPatterns[] = sprintf(self::ORDER_BY_PATH_EXPRESSION, $sqlTableAliasForFieldAlias, $columnName); | ||
| 426 | $replacements[] = $fieldAlias; | ||
| 427 | } | ||
| 428 | |||
| 429 | return [$searchPatterns, $replacements]; | ||
| 430 | } | ||
| 431 | |||
| 432 | /** | ||
| 433 | * getter for $orderByPathExpressions | ||
| 434 | * | ||
| 435 | * @return list<PathExpression> | ||
| 436 | */ | ||
| 437 | public function getOrderByPathExpressions(): array | ||
| 438 | { | ||
| 439 | return $this->orderByPathExpressions; | ||
| 440 | } | ||
| 441 | |||
| 442 | /** | ||
| 443 | * @throws OptimisticLockException | ||
| 444 | * @throws QueryException | ||
| 445 | */ | ||
| 446 | private function getInnerSQL(SelectStatement $AST): string | ||
| 447 | { | ||
| 448 | // Set every select expression as visible(hidden = false) to | ||
| 449 | // make $AST have scalar mappings properly - this is relevant for referencing selected | ||
| 450 | // fields from outside the subquery, for example in the ORDER BY segment | ||
| 451 | $hiddens = []; | ||
| 452 | |||
| 453 | foreach ($AST->selectClause->selectExpressions as $idx => $expr) { | ||
| 454 | $hiddens[$idx] = $expr->hiddenAliasResultVariable; | ||
| 455 | $expr->hiddenAliasResultVariable = false; | ||
| 456 | } | ||
| 457 | |||
| 458 | $innerSql = parent::walkSelectStatement($AST); | ||
| 459 | |||
| 460 | // Restore hiddens | ||
| 461 | foreach ($AST->selectClause->selectExpressions as $idx => $expr) { | ||
| 462 | $expr->hiddenAliasResultVariable = $hiddens[$idx]; | ||
| 463 | } | ||
| 464 | |||
| 465 | return $innerSql; | ||
| 466 | } | ||
| 467 | |||
| 468 | /** @return string[] */ | ||
| 469 | private function getSQLIdentifier(SelectStatement $AST): array | ||
| 470 | { | ||
| 471 | // Find out the SQL alias of the identifier column of the root entity. | ||
| 472 | // It may be possible to make this work with multiple root entities but that | ||
| 473 | // would probably require issuing multiple queries or doing a UNION SELECT. | ||
| 474 | // So for now, it's not supported. | ||
| 475 | |||
| 476 | // Get the root entity and alias from the AST fromClause. | ||
| 477 | $from = $AST->fromClause->identificationVariableDeclarations; | ||
| 478 | if (count($from) !== 1) { | ||
| 479 | throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction'); | ||
| 480 | } | ||
| 481 | |||
| 482 | $fromRoot = reset($from); | ||
| 483 | $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; | ||
| 484 | $rootClass = $this->getMetadataForDqlAlias($rootAlias); | ||
| 485 | $rootIdentifier = $rootClass->identifier; | ||
| 486 | |||
| 487 | // For every identifier, find out the SQL alias by combing through the ResultSetMapping | ||
| 488 | $sqlIdentifier = []; | ||
| 489 | foreach ($rootIdentifier as $property) { | ||
| 490 | if (isset($rootClass->fieldMappings[$property])) { | ||
| 491 | foreach (array_keys($this->rsm->fieldMappings, $property, true) as $alias) { | ||
| 492 | if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) { | ||
| 493 | $sqlIdentifier[$property] = $alias; | ||
| 494 | } | ||
| 495 | } | ||
| 496 | } | ||
| 497 | |||
| 498 | if (isset($rootClass->associationMappings[$property])) { | ||
| 499 | $association = $rootClass->associationMappings[$property]; | ||
| 500 | assert($association->isToOneOwningSide()); | ||
| 501 | $joinColumn = $association->joinColumns[0]->name; | ||
| 502 | |||
| 503 | foreach (array_keys($this->rsm->metaMappings, $joinColumn, true) as $alias) { | ||
| 504 | if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) { | ||
| 505 | $sqlIdentifier[$property] = $alias; | ||
| 506 | } | ||
| 507 | } | ||
| 508 | } | ||
| 509 | } | ||
| 510 | |||
| 511 | if (count($sqlIdentifier) === 0) { | ||
| 512 | throw new RuntimeException('The Paginator does not support Queries which only yield ScalarResults.'); | ||
| 513 | } | ||
| 514 | |||
| 515 | if (count($rootIdentifier) !== count($sqlIdentifier)) { | ||
| 516 | throw new RuntimeException(sprintf( | ||
| 517 | 'Not all identifier properties can be found in the ResultSetMapping: %s', | ||
| 518 | implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier))), | ||
| 519 | )); | ||
| 520 | } | ||
| 521 | |||
| 522 | return $sqlIdentifier; | ||
| 523 | } | ||
| 524 | |||
| 525 | public function walkPathExpression(PathExpression $pathExpr): string | ||
| 526 | { | ||
| 527 | if (! $this->inSubSelect && ! $this->platformSupportsRowNumber() && ! in_array($pathExpr, $this->orderByPathExpressions, true)) { | ||
| 528 | $this->orderByPathExpressions[] = $pathExpr; | ||
| 529 | } | ||
| 530 | |||
| 531 | return parent::walkPathExpression($pathExpr); | ||
| 532 | } | ||
| 533 | |||
| 534 | public function walkSubSelect(Subselect $subselect): string | ||
| 535 | { | ||
| 536 | $this->inSubSelect = true; | ||
| 537 | |||
| 538 | $sql = parent::walkSubselect($subselect); | ||
| 539 | |||
| 540 | $this->inSubSelect = false; | ||
| 541 | |||
| 542 | return $sql; | ||
| 543 | } | ||
| 544 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Pagination; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Types\Type; | ||
| 8 | use Doctrine\ORM\Query; | ||
| 9 | use Doctrine\ORM\Query\AST\Functions\IdentityFunction; | ||
| 10 | use Doctrine\ORM\Query\AST\Node; | ||
| 11 | use Doctrine\ORM\Query\AST\PathExpression; | ||
| 12 | use Doctrine\ORM\Query\AST\SelectExpression; | ||
| 13 | use Doctrine\ORM\Query\AST\SelectStatement; | ||
| 14 | use Doctrine\ORM\Query\TreeWalkerAdapter; | ||
| 15 | use RuntimeException; | ||
| 16 | |||
| 17 | use function count; | ||
| 18 | use function is_string; | ||
| 19 | use function reset; | ||
| 20 | |||
| 21 | /** | ||
| 22 | * Replaces the selectClause of the AST with a SELECT DISTINCT root.id equivalent. | ||
| 23 | */ | ||
| 24 | class LimitSubqueryWalker extends TreeWalkerAdapter | ||
| 25 | { | ||
| 26 | public const IDENTIFIER_TYPE = 'doctrine_paginator.id.type'; | ||
| 27 | |||
| 28 | public const FORCE_DBAL_TYPE_CONVERSION = 'doctrine_paginator.scalar_result.force_dbal_type_conversion'; | ||
| 29 | |||
| 30 | /** | ||
| 31 | * Counter for generating unique order column aliases. | ||
| 32 | */ | ||
| 33 | private int $aliasCounter = 0; | ||
| 34 | |||
| 35 | public function walkSelectStatement(SelectStatement $selectStatement): void | ||
| 36 | { | ||
| 37 | // Get the root entity and alias from the AST fromClause | ||
| 38 | $from = $selectStatement->fromClause->identificationVariableDeclarations; | ||
| 39 | $fromRoot = reset($from); | ||
| 40 | $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; | ||
| 41 | $rootClass = $this->getMetadataForDqlAlias($rootAlias); | ||
| 42 | |||
| 43 | $this->validate($selectStatement); | ||
| 44 | $identifier = $rootClass->getSingleIdentifierFieldName(); | ||
| 45 | |||
| 46 | if (isset($rootClass->associationMappings[$identifier])) { | ||
| 47 | 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.'); | ||
| 48 | } | ||
| 49 | |||
| 50 | $query = $this->_getQuery(); | ||
| 51 | |||
| 52 | $query->setHint( | ||
| 53 | self::IDENTIFIER_TYPE, | ||
| 54 | Type::getType($rootClass->fieldMappings[$identifier]->type), | ||
| 55 | ); | ||
| 56 | |||
| 57 | $query->setHint(self::FORCE_DBAL_TYPE_CONVERSION, true); | ||
| 58 | |||
| 59 | $pathExpression = new PathExpression( | ||
| 60 | PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, | ||
| 61 | $rootAlias, | ||
| 62 | $identifier, | ||
| 63 | ); | ||
| 64 | |||
| 65 | $pathExpression->type = PathExpression::TYPE_STATE_FIELD; | ||
| 66 | |||
| 67 | $selectStatement->selectClause->selectExpressions = [new SelectExpression($pathExpression, '_dctrn_id')]; | ||
| 68 | $selectStatement->selectClause->isDistinct = ($query->getHints()[Paginator::HINT_ENABLE_DISTINCT] ?? true) === true; | ||
| 69 | |||
| 70 | if (! isset($selectStatement->orderByClause)) { | ||
| 71 | return; | ||
| 72 | } | ||
| 73 | |||
| 74 | $queryComponents = $this->getQueryComponents(); | ||
| 75 | foreach ($selectStatement->orderByClause->orderByItems as $item) { | ||
| 76 | if ($item->expression instanceof PathExpression) { | ||
| 77 | $selectStatement->selectClause->selectExpressions[] = new SelectExpression( | ||
| 78 | $this->createSelectExpressionItem($item->expression), | ||
| 79 | '_dctrn_ord' . $this->aliasCounter++, | ||
| 80 | ); | ||
| 81 | |||
| 82 | continue; | ||
| 83 | } | ||
| 84 | |||
| 85 | if (is_string($item->expression) && isset($queryComponents[$item->expression])) { | ||
| 86 | $qComp = $queryComponents[$item->expression]; | ||
| 87 | |||
| 88 | if (isset($qComp['resultVariable'])) { | ||
| 89 | $selectStatement->selectClause->selectExpressions[] = new SelectExpression( | ||
| 90 | $qComp['resultVariable'], | ||
| 91 | $item->expression, | ||
| 92 | ); | ||
| 93 | } | ||
| 94 | } | ||
| 95 | } | ||
| 96 | } | ||
| 97 | |||
| 98 | /** | ||
| 99 | * Validate the AST to ensure that this walker is able to properly manipulate it. | ||
| 100 | */ | ||
| 101 | private function validate(SelectStatement $AST): void | ||
| 102 | { | ||
| 103 | // Prevent LimitSubqueryWalker from being used with queries that include | ||
| 104 | // a limit, a fetched to-many join, and an order by condition that | ||
| 105 | // references a column from the fetch joined table. | ||
| 106 | $queryComponents = $this->getQueryComponents(); | ||
| 107 | $query = $this->_getQuery(); | ||
| 108 | $from = $AST->fromClause->identificationVariableDeclarations; | ||
| 109 | $fromRoot = reset($from); | ||
| 110 | |||
| 111 | if ( | ||
| 112 | $query instanceof Query | ||
| 113 | && $query->getMaxResults() !== null | ||
| 114 | && $AST->orderByClause | ||
| 115 | && count($fromRoot->joins) | ||
| 116 | ) { | ||
| 117 | // Check each orderby item. | ||
| 118 | // TODO: check complex orderby items too... | ||
| 119 | foreach ($AST->orderByClause->orderByItems as $orderByItem) { | ||
| 120 | $expression = $orderByItem->expression; | ||
| 121 | if ( | ||
| 122 | $orderByItem->expression instanceof PathExpression | ||
| 123 | && isset($queryComponents[$expression->identificationVariable]) | ||
| 124 | ) { | ||
| 125 | $queryComponent = $queryComponents[$expression->identificationVariable]; | ||
| 126 | if ( | ||
| 127 | isset($queryComponent['parent']) | ||
| 128 | && isset($queryComponent['relation']) | ||
| 129 | && $queryComponent['relation']->isToMany() | ||
| 130 | ) { | ||
| 131 | 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.'); | ||
| 132 | } | ||
| 133 | } | ||
| 134 | } | ||
| 135 | } | ||
| 136 | } | ||
| 137 | |||
| 138 | /** | ||
| 139 | * Retrieve either an IdentityFunction (IDENTITY(u.assoc)) or a state field (u.name). | ||
| 140 | * | ||
| 141 | * @return IdentityFunction|PathExpression | ||
| 142 | */ | ||
| 143 | private function createSelectExpressionItem(PathExpression $pathExpression): Node | ||
| 144 | { | ||
| 145 | if ($pathExpression->type === PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) { | ||
| 146 | $identity = new IdentityFunction('identity'); | ||
| 147 | |||
| 148 | $identity->pathExpression = clone $pathExpression; | ||
| 149 | |||
| 150 | return $identity; | ||
| 151 | } | ||
| 152 | |||
| 153 | return clone $pathExpression; | ||
| 154 | } | ||
| 155 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Pagination; | ||
| 6 | |||
| 7 | use ArrayIterator; | ||
| 8 | use Countable; | ||
| 9 | use Doctrine\Common\Collections\Collection; | ||
| 10 | use Doctrine\ORM\Internal\SQLResultCasing; | ||
| 11 | use Doctrine\ORM\NoResultException; | ||
| 12 | use Doctrine\ORM\Query; | ||
| 13 | use Doctrine\ORM\Query\Parameter; | ||
| 14 | use Doctrine\ORM\Query\Parser; | ||
| 15 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 16 | use Doctrine\ORM\QueryBuilder; | ||
| 17 | use IteratorAggregate; | ||
| 18 | use Traversable; | ||
| 19 | |||
| 20 | use function array_key_exists; | ||
| 21 | use function array_map; | ||
| 22 | use function array_sum; | ||
| 23 | use function assert; | ||
| 24 | use function is_string; | ||
| 25 | |||
| 26 | /** | ||
| 27 | * The paginator can handle various complex scenarios with DQL. | ||
| 28 | * | ||
| 29 | * @template-covariant T | ||
| 30 | * @implements IteratorAggregate<array-key, T> | ||
| 31 | */ | ||
| 32 | class Paginator implements Countable, IteratorAggregate | ||
| 33 | { | ||
| 34 | use SQLResultCasing; | ||
| 35 | |||
| 36 | public const HINT_ENABLE_DISTINCT = 'paginator.distinct.enable'; | ||
| 37 | |||
| 38 | private readonly Query $query; | ||
| 39 | private bool|null $useOutputWalkers = null; | ||
| 40 | private int|null $count = null; | ||
| 41 | |||
| 42 | /** @param bool $fetchJoinCollection Whether the query joins a collection (true by default). */ | ||
| 43 | public function __construct( | ||
| 44 | Query|QueryBuilder $query, | ||
| 45 | private readonly bool $fetchJoinCollection = true, | ||
| 46 | ) { | ||
| 47 | if ($query instanceof QueryBuilder) { | ||
| 48 | $query = $query->getQuery(); | ||
| 49 | } | ||
| 50 | |||
| 51 | $this->query = $query; | ||
| 52 | } | ||
| 53 | |||
| 54 | /** | ||
| 55 | * Returns the query. | ||
| 56 | */ | ||
| 57 | public function getQuery(): Query | ||
| 58 | { | ||
| 59 | return $this->query; | ||
| 60 | } | ||
| 61 | |||
| 62 | /** | ||
| 63 | * Returns whether the query joins a collection. | ||
| 64 | * | ||
| 65 | * @return bool Whether the query joins a collection. | ||
| 66 | */ | ||
| 67 | public function getFetchJoinCollection(): bool | ||
| 68 | { | ||
| 69 | return $this->fetchJoinCollection; | ||
| 70 | } | ||
| 71 | |||
| 72 | /** | ||
| 73 | * Returns whether the paginator will use an output walker. | ||
| 74 | */ | ||
| 75 | public function getUseOutputWalkers(): bool|null | ||
| 76 | { | ||
| 77 | return $this->useOutputWalkers; | ||
| 78 | } | ||
| 79 | |||
| 80 | /** | ||
| 81 | * Sets whether the paginator will use an output walker. | ||
| 82 | * | ||
| 83 | * @return $this | ||
| 84 | */ | ||
| 85 | public function setUseOutputWalkers(bool|null $useOutputWalkers): static | ||
| 86 | { | ||
| 87 | $this->useOutputWalkers = $useOutputWalkers; | ||
| 88 | |||
| 89 | return $this; | ||
| 90 | } | ||
| 91 | |||
| 92 | public function count(): int | ||
| 93 | { | ||
| 94 | if ($this->count === null) { | ||
| 95 | try { | ||
| 96 | $this->count = (int) array_sum(array_map('current', $this->getCountQuery()->getScalarResult())); | ||
| 97 | } catch (NoResultException) { | ||
| 98 | $this->count = 0; | ||
| 99 | } | ||
| 100 | } | ||
| 101 | |||
| 102 | return $this->count; | ||
| 103 | } | ||
| 104 | |||
| 105 | /** | ||
| 106 | * {@inheritDoc} | ||
| 107 | * | ||
| 108 | * @psalm-return Traversable<array-key, T> | ||
| 109 | */ | ||
| 110 | public function getIterator(): Traversable | ||
| 111 | { | ||
| 112 | $offset = $this->query->getFirstResult(); | ||
| 113 | $length = $this->query->getMaxResults(); | ||
| 114 | |||
| 115 | if ($this->fetchJoinCollection && $length !== null) { | ||
| 116 | $subQuery = $this->cloneQuery($this->query); | ||
| 117 | |||
| 118 | if ($this->useOutputWalker($subQuery)) { | ||
| 119 | $subQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class); | ||
| 120 | } else { | ||
| 121 | $this->appendTreeWalker($subQuery, LimitSubqueryWalker::class); | ||
| 122 | $this->unbindUnusedQueryParams($subQuery); | ||
| 123 | } | ||
| 124 | |||
| 125 | $subQuery->setFirstResult($offset)->setMaxResults($length); | ||
| 126 | |||
| 127 | $foundIdRows = $subQuery->getScalarResult(); | ||
| 128 | |||
| 129 | // don't do this for an empty id array | ||
| 130 | if ($foundIdRows === []) { | ||
| 131 | return new ArrayIterator([]); | ||
| 132 | } | ||
| 133 | |||
| 134 | $whereInQuery = $this->cloneQuery($this->query); | ||
| 135 | $ids = array_map('current', $foundIdRows); | ||
| 136 | |||
| 137 | $this->appendTreeWalker($whereInQuery, WhereInWalker::class); | ||
| 138 | $whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_HAS_IDS, true); | ||
| 139 | $whereInQuery->setFirstResult(0)->setMaxResults(null); | ||
| 140 | $whereInQuery->setCacheable($this->query->isCacheable()); | ||
| 141 | |||
| 142 | $databaseIds = $this->convertWhereInIdentifiersToDatabaseValues($ids); | ||
| 143 | $whereInQuery->setParameter(WhereInWalker::PAGINATOR_ID_ALIAS, $databaseIds); | ||
| 144 | |||
| 145 | $result = $whereInQuery->getResult($this->query->getHydrationMode()); | ||
| 146 | } else { | ||
| 147 | $result = $this->cloneQuery($this->query) | ||
| 148 | ->setMaxResults($length) | ||
| 149 | ->setFirstResult($offset) | ||
| 150 | ->setCacheable($this->query->isCacheable()) | ||
| 151 | ->getResult($this->query->getHydrationMode()); | ||
| 152 | } | ||
| 153 | |||
| 154 | return new ArrayIterator($result); | ||
| 155 | } | ||
| 156 | |||
| 157 | private function cloneQuery(Query $query): Query | ||
| 158 | { | ||
| 159 | $cloneQuery = clone $query; | ||
| 160 | |||
| 161 | $cloneQuery->setParameters(clone $query->getParameters()); | ||
| 162 | $cloneQuery->setCacheable(false); | ||
| 163 | |||
| 164 | foreach ($query->getHints() as $name => $value) { | ||
| 165 | $cloneQuery->setHint($name, $value); | ||
| 166 | } | ||
| 167 | |||
| 168 | return $cloneQuery; | ||
| 169 | } | ||
| 170 | |||
| 171 | /** | ||
| 172 | * Determines whether to use an output walker for the query. | ||
| 173 | */ | ||
| 174 | private function useOutputWalker(Query $query): bool | ||
| 175 | { | ||
| 176 | if ($this->useOutputWalkers === null) { | ||
| 177 | return (bool) $query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER) === false; | ||
| 178 | } | ||
| 179 | |||
| 180 | return $this->useOutputWalkers; | ||
| 181 | } | ||
| 182 | |||
| 183 | /** | ||
| 184 | * Appends a custom tree walker to the tree walkers hint. | ||
| 185 | * | ||
| 186 | * @psalm-param class-string $walkerClass | ||
| 187 | */ | ||
| 188 | private function appendTreeWalker(Query $query, string $walkerClass): void | ||
| 189 | { | ||
| 190 | $hints = $query->getHint(Query::HINT_CUSTOM_TREE_WALKERS); | ||
| 191 | |||
| 192 | if ($hints === false) { | ||
| 193 | $hints = []; | ||
| 194 | } | ||
| 195 | |||
| 196 | $hints[] = $walkerClass; | ||
| 197 | $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, $hints); | ||
| 198 | } | ||
| 199 | |||
| 200 | /** | ||
| 201 | * Returns Query prepared to count. | ||
| 202 | */ | ||
| 203 | private function getCountQuery(): Query | ||
| 204 | { | ||
| 205 | $countQuery = $this->cloneQuery($this->query); | ||
| 206 | |||
| 207 | if (! $countQuery->hasHint(CountWalker::HINT_DISTINCT)) { | ||
| 208 | $countQuery->setHint(CountWalker::HINT_DISTINCT, true); | ||
| 209 | } | ||
| 210 | |||
| 211 | if ($this->useOutputWalker($countQuery)) { | ||
| 212 | $platform = $countQuery->getEntityManager()->getConnection()->getDatabasePlatform(); // law of demeter win | ||
| 213 | |||
| 214 | $rsm = new ResultSetMapping(); | ||
| 215 | $rsm->addScalarResult($this->getSQLResultCasing($platform, 'dctrn_count'), 'count'); | ||
| 216 | |||
| 217 | $countQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, CountOutputWalker::class); | ||
| 218 | $countQuery->setResultSetMapping($rsm); | ||
| 219 | } else { | ||
| 220 | $this->appendTreeWalker($countQuery, CountWalker::class); | ||
| 221 | $this->unbindUnusedQueryParams($countQuery); | ||
| 222 | } | ||
| 223 | |||
| 224 | $countQuery->setFirstResult(0)->setMaxResults(null); | ||
| 225 | |||
| 226 | return $countQuery; | ||
| 227 | } | ||
| 228 | |||
| 229 | private function unbindUnusedQueryParams(Query $query): void | ||
| 230 | { | ||
| 231 | $parser = new Parser($query); | ||
| 232 | $parameterMappings = $parser->parse()->getParameterMappings(); | ||
| 233 | /** @var Collection|Parameter[] $parameters */ | ||
| 234 | $parameters = $query->getParameters(); | ||
| 235 | |||
| 236 | foreach ($parameters as $key => $parameter) { | ||
| 237 | $parameterName = $parameter->getName(); | ||
| 238 | |||
| 239 | if (! (isset($parameterMappings[$parameterName]) || array_key_exists($parameterName, $parameterMappings))) { | ||
| 240 | unset($parameters[$key]); | ||
| 241 | } | ||
| 242 | } | ||
| 243 | |||
| 244 | $query->setParameters($parameters); | ||
| 245 | } | ||
| 246 | |||
| 247 | /** | ||
| 248 | * @param mixed[] $identifiers | ||
| 249 | * | ||
| 250 | * @return mixed[] | ||
| 251 | */ | ||
| 252 | private function convertWhereInIdentifiersToDatabaseValues(array $identifiers): array | ||
| 253 | { | ||
| 254 | $query = $this->cloneQuery($this->query); | ||
| 255 | $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, RootTypeWalker::class); | ||
| 256 | |||
| 257 | $connection = $this->query->getEntityManager()->getConnection(); | ||
| 258 | $type = $query->getSQL(); | ||
| 259 | assert(is_string($type)); | ||
| 260 | |||
| 261 | return array_map(static fn ($id): mixed => $connection->convertToDatabaseValue($id, $type), $identifiers); | ||
| 262 | } | ||
| 263 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Pagination; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST; | ||
| 8 | use Doctrine\ORM\Query\SqlWalker; | ||
| 9 | use Doctrine\ORM\Utility\PersisterHelper; | ||
| 10 | use RuntimeException; | ||
| 11 | |||
| 12 | use function count; | ||
| 13 | use function reset; | ||
| 14 | |||
| 15 | /** | ||
| 16 | * Infers the DBAL type of the #Id (identifier) column of the given query's root entity, and | ||
| 17 | * returns it in place of a real SQL statement. | ||
| 18 | * | ||
| 19 | * Obtaining this type is a necessary intermediate step for \Doctrine\ORM\Tools\Pagination\Paginator. | ||
| 20 | * We can best do this from a tree walker because it gives us access to the AST. | ||
| 21 | * | ||
| 22 | * Returning the type instead of a "real" SQL statement is a slight hack. However, it has the | ||
| 23 | * benefit that the DQL -> root entity id type resolution can be cached in the query cache. | ||
| 24 | */ | ||
| 25 | final class RootTypeWalker extends SqlWalker | ||
| 26 | { | ||
| 27 | public function walkSelectStatement(AST\SelectStatement $selectStatement): string | ||
| 28 | { | ||
| 29 | // Get the root entity and alias from the AST fromClause | ||
| 30 | $from = $selectStatement->fromClause->identificationVariableDeclarations; | ||
| 31 | |||
| 32 | if (count($from) > 1) { | ||
| 33 | throw new RuntimeException('Can only process queries that select only one FROM component'); | ||
| 34 | } | ||
| 35 | |||
| 36 | $fromRoot = reset($from); | ||
| 37 | $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; | ||
| 38 | $rootClass = $this->getMetadataForDqlAlias($rootAlias); | ||
| 39 | $identifierFieldName = $rootClass->getSingleIdentifierFieldName(); | ||
| 40 | |||
| 41 | return PersisterHelper::getTypeOfField( | ||
| 42 | $identifierFieldName, | ||
| 43 | $rootClass, | ||
| 44 | $this->getQuery() | ||
| 45 | ->getEntityManager(), | ||
| 46 | )[0]; | ||
| 47 | } | ||
| 48 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Pagination; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||
| 8 | use Doctrine\ORM\Query\AST\OrderByClause; | ||
| 9 | use Doctrine\ORM\Query\Parser; | ||
| 10 | use Doctrine\ORM\Query\SqlWalker; | ||
| 11 | use Doctrine\ORM\Tools\Pagination\Exception\RowNumberOverFunctionNotEnabled; | ||
| 12 | |||
| 13 | use function trim; | ||
| 14 | |||
| 15 | /** | ||
| 16 | * RowNumberOverFunction | ||
| 17 | * | ||
| 18 | * Provides ROW_NUMBER() OVER(ORDER BY...) construct for use in LimitSubqueryOutputWalker | ||
| 19 | */ | ||
| 20 | class RowNumberOverFunction extends FunctionNode | ||
| 21 | { | ||
| 22 | public OrderByClause $orderByClause; | ||
| 23 | |||
| 24 | public function getSql(SqlWalker $sqlWalker): string | ||
| 25 | { | ||
| 26 | return 'ROW_NUMBER() OVER(' . trim($sqlWalker->walkOrderByClause( | ||
| 27 | $this->orderByClause, | ||
| 28 | )) . ')'; | ||
| 29 | } | ||
| 30 | |||
| 31 | /** | ||
| 32 | * @throws RowNumberOverFunctionNotEnabled | ||
| 33 | * | ||
| 34 | * @inheritdoc | ||
| 35 | */ | ||
| 36 | public function parse(Parser $parser): void | ||
| 37 | { | ||
| 38 | throw RowNumberOverFunctionNotEnabled::create(); | ||
| 39 | } | ||
| 40 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Pagination; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\ArithmeticExpression; | ||
| 8 | use Doctrine\ORM\Query\AST\ConditionalExpression; | ||
| 9 | use Doctrine\ORM\Query\AST\ConditionalPrimary; | ||
| 10 | use Doctrine\ORM\Query\AST\ConditionalTerm; | ||
| 11 | use Doctrine\ORM\Query\AST\InListExpression; | ||
| 12 | use Doctrine\ORM\Query\AST\InputParameter; | ||
| 13 | use Doctrine\ORM\Query\AST\NullComparisonExpression; | ||
| 14 | use Doctrine\ORM\Query\AST\PathExpression; | ||
| 15 | use Doctrine\ORM\Query\AST\SelectStatement; | ||
| 16 | use Doctrine\ORM\Query\AST\SimpleArithmeticExpression; | ||
| 17 | use Doctrine\ORM\Query\AST\WhereClause; | ||
| 18 | use Doctrine\ORM\Query\TreeWalkerAdapter; | ||
| 19 | use RuntimeException; | ||
| 20 | |||
| 21 | use function count; | ||
| 22 | use function reset; | ||
| 23 | |||
| 24 | /** | ||
| 25 | * Appends a condition equivalent to "WHERE IN (:dpid_1, :dpid_2, ...)" to the whereClause of the AST. | ||
| 26 | * | ||
| 27 | * The parameter namespace (dpid) is defined by | ||
| 28 | * the PAGINATOR_ID_ALIAS | ||
| 29 | * | ||
| 30 | * The HINT_PAGINATOR_HAS_IDS query hint indicates whether there are | ||
| 31 | * any ids in the parameter at all. | ||
| 32 | */ | ||
| 33 | class WhereInWalker extends TreeWalkerAdapter | ||
| 34 | { | ||
| 35 | /** | ||
| 36 | * ID Count hint name. | ||
| 37 | */ | ||
| 38 | public const HINT_PAGINATOR_HAS_IDS = 'doctrine.paginator_has_ids'; | ||
| 39 | |||
| 40 | /** | ||
| 41 | * Primary key alias for query. | ||
| 42 | */ | ||
| 43 | public const PAGINATOR_ID_ALIAS = 'dpid'; | ||
| 44 | |||
| 45 | public function walkSelectStatement(SelectStatement $selectStatement): void | ||
| 46 | { | ||
| 47 | // Get the root entity and alias from the AST fromClause | ||
| 48 | $from = $selectStatement->fromClause->identificationVariableDeclarations; | ||
| 49 | |||
| 50 | if (count($from) > 1) { | ||
| 51 | throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction'); | ||
| 52 | } | ||
| 53 | |||
| 54 | $fromRoot = reset($from); | ||
| 55 | $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; | ||
| 56 | $rootClass = $this->getMetadataForDqlAlias($rootAlias); | ||
| 57 | $identifierFieldName = $rootClass->getSingleIdentifierFieldName(); | ||
| 58 | |||
| 59 | $pathType = PathExpression::TYPE_STATE_FIELD; | ||
| 60 | if (isset($rootClass->associationMappings[$identifierFieldName])) { | ||
| 61 | $pathType = PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; | ||
| 62 | } | ||
| 63 | |||
| 64 | $pathExpression = new PathExpression(PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $rootAlias, $identifierFieldName); | ||
| 65 | $pathExpression->type = $pathType; | ||
| 66 | |||
| 67 | $hasIds = $this->_getQuery()->getHint(self::HINT_PAGINATOR_HAS_IDS); | ||
| 68 | |||
| 69 | if ($hasIds) { | ||
| 70 | $arithmeticExpression = new ArithmeticExpression(); | ||
| 71 | $arithmeticExpression->simpleArithmeticExpression = new SimpleArithmeticExpression( | ||
| 72 | [$pathExpression], | ||
| 73 | ); | ||
| 74 | $expression = new InListExpression( | ||
| 75 | $arithmeticExpression, | ||
| 76 | [new InputParameter(':' . self::PAGINATOR_ID_ALIAS)], | ||
| 77 | ); | ||
| 78 | } else { | ||
| 79 | $expression = new NullComparisonExpression($pathExpression); | ||
| 80 | } | ||
| 81 | |||
| 82 | $conditionalPrimary = new ConditionalPrimary(); | ||
| 83 | $conditionalPrimary->simpleConditionalExpression = $expression; | ||
| 84 | if ($selectStatement->whereClause) { | ||
| 85 | if ($selectStatement->whereClause->conditionalExpression instanceof ConditionalTerm) { | ||
| 86 | $selectStatement->whereClause->conditionalExpression->conditionalFactors[] = $conditionalPrimary; | ||
| 87 | } elseif ($selectStatement->whereClause->conditionalExpression instanceof ConditionalPrimary) { | ||
| 88 | $selectStatement->whereClause->conditionalExpression = new ConditionalExpression( | ||
| 89 | [ | ||
| 90 | new ConditionalTerm( | ||
| 91 | [ | ||
| 92 | $selectStatement->whereClause->conditionalExpression, | ||
| 93 | $conditionalPrimary, | ||
| 94 | ], | ||
| 95 | ), | ||
| 96 | ], | ||
| 97 | ); | ||
| 98 | } else { | ||
| 99 | $tmpPrimary = new ConditionalPrimary(); | ||
| 100 | $tmpPrimary->conditionalExpression = $selectStatement->whereClause->conditionalExpression; | ||
| 101 | $selectStatement->whereClause->conditionalExpression = new ConditionalTerm( | ||
| 102 | [ | ||
| 103 | $tmpPrimary, | ||
| 104 | $conditionalPrimary, | ||
| 105 | ], | ||
| 106 | ); | ||
| 107 | } | ||
| 108 | } else { | ||
| 109 | $selectStatement->whereClause = new WhereClause( | ||
| 110 | new ConditionalExpression( | ||
| 111 | [new ConditionalTerm([$conditionalPrimary])], | ||
| 112 | ), | ||
| 113 | ); | ||
| 114 | } | ||
| 115 | } | ||
| 116 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools; | ||
| 6 | |||
| 7 | use Doctrine\Common\EventSubscriber; | ||
| 8 | use Doctrine\ORM\Event\LoadClassMetadataEventArgs; | ||
| 9 | use Doctrine\ORM\Event\OnClassMetadataNotFoundEventArgs; | ||
| 10 | use Doctrine\ORM\Events; | ||
| 11 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 12 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 13 | |||
| 14 | use function array_key_exists; | ||
| 15 | use function array_replace_recursive; | ||
| 16 | use function ltrim; | ||
| 17 | |||
| 18 | /** | ||
| 19 | * ResolveTargetEntityListener | ||
| 20 | * | ||
| 21 | * Mechanism to overwrite interfaces or classes specified as association | ||
| 22 | * targets. | ||
| 23 | */ | ||
| 24 | class ResolveTargetEntityListener implements EventSubscriber | ||
| 25 | { | ||
| 26 | /** @var mixed[][] indexed by original entity name */ | ||
| 27 | private array $resolveTargetEntities = []; | ||
| 28 | |||
| 29 | /** | ||
| 30 | * {@inheritDoc} | ||
| 31 | */ | ||
| 32 | public function getSubscribedEvents(): array | ||
| 33 | { | ||
| 34 | return [ | ||
| 35 | Events::loadClassMetadata, | ||
| 36 | Events::onClassMetadataNotFound, | ||
| 37 | ]; | ||
| 38 | } | ||
| 39 | |||
| 40 | /** | ||
| 41 | * Adds a target-entity class name to resolve to a new class name. | ||
| 42 | * | ||
| 43 | * @psalm-param array<string, mixed> $mapping | ||
| 44 | */ | ||
| 45 | public function addResolveTargetEntity(string $originalEntity, string $newEntity, array $mapping): void | ||
| 46 | { | ||
| 47 | $mapping['targetEntity'] = ltrim($newEntity, '\\'); | ||
| 48 | $this->resolveTargetEntities[ltrim($originalEntity, '\\')] = $mapping; | ||
| 49 | } | ||
| 50 | |||
| 51 | /** @internal this is an event callback, and should not be called directly */ | ||
| 52 | public function onClassMetadataNotFound(OnClassMetadataNotFoundEventArgs $args): void | ||
| 53 | { | ||
| 54 | if (array_key_exists($args->getClassName(), $this->resolveTargetEntities)) { | ||
| 55 | $args->setFoundMetadata( | ||
| 56 | $args | ||
| 57 | ->getObjectManager() | ||
| 58 | ->getClassMetadata($this->resolveTargetEntities[$args->getClassName()]['targetEntity']), | ||
| 59 | ); | ||
| 60 | } | ||
| 61 | } | ||
| 62 | |||
| 63 | /** | ||
| 64 | * Processes event and resolves new target entity names. | ||
| 65 | * | ||
| 66 | * @internal this is an event callback, and should not be called directly | ||
| 67 | */ | ||
| 68 | public function loadClassMetadata(LoadClassMetadataEventArgs $args): void | ||
| 69 | { | ||
| 70 | $cm = $args->getClassMetadata(); | ||
| 71 | |||
| 72 | foreach ($cm->associationMappings as $mapping) { | ||
| 73 | if (isset($this->resolveTargetEntities[$mapping->targetEntity])) { | ||
| 74 | $this->remapAssociation($cm, $mapping); | ||
| 75 | } | ||
| 76 | } | ||
| 77 | |||
| 78 | foreach ($this->resolveTargetEntities as $interface => $data) { | ||
| 79 | if ($data['targetEntity'] === $cm->getName()) { | ||
| 80 | $args->getEntityManager()->getMetadataFactory()->setMetadataFor($interface, $cm); | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | foreach ($cm->discriminatorMap as $value => $class) { | ||
| 85 | if (isset($this->resolveTargetEntities[$class])) { | ||
| 86 | $cm->addDiscriminatorMapClass($value, $this->resolveTargetEntities[$class]['targetEntity']); | ||
| 87 | } | ||
| 88 | } | ||
| 89 | } | ||
| 90 | |||
| 91 | private function remapAssociation(ClassMetadata $classMetadata, AssociationMapping $mapping): void | ||
| 92 | { | ||
| 93 | $newMapping = $this->resolveTargetEntities[$mapping->targetEntity]; | ||
| 94 | $newMapping = array_replace_recursive( | ||
| 95 | $mapping->toArray(), | ||
| 96 | $newMapping, | ||
| 97 | ); | ||
| 98 | $newMapping['fieldName'] = $mapping->fieldName; | ||
| 99 | |||
| 100 | unset($classMetadata->associationMappings[$mapping->fieldName]); | ||
| 101 | |||
| 102 | switch ($mapping->type()) { | ||
| 103 | case ClassMetadata::MANY_TO_MANY: | ||
| 104 | $classMetadata->mapManyToMany($newMapping); | ||
| 105 | break; | ||
| 106 | case ClassMetadata::MANY_TO_ONE: | ||
| 107 | $classMetadata->mapManyToOne($newMapping); | ||
| 108 | break; | ||
| 109 | case ClassMetadata::ONE_TO_MANY: | ||
| 110 | $classMetadata->mapOneToMany($newMapping); | ||
| 111 | break; | ||
| 112 | case ClassMetadata::ONE_TO_ONE: | ||
| 113 | $classMetadata->mapOneToOne($newMapping); | ||
| 114 | break; | ||
| 115 | } | ||
| 116 | } | ||
| 117 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools; | ||
| 6 | |||
| 7 | use BackedEnum; | ||
| 8 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 9 | use Doctrine\DBAL\Schema\AbstractAsset; | ||
| 10 | use Doctrine\DBAL\Schema\AbstractSchemaManager; | ||
| 11 | use Doctrine\DBAL\Schema\Index; | ||
| 12 | use Doctrine\DBAL\Schema\Schema; | ||
| 13 | use Doctrine\DBAL\Schema\Table; | ||
| 14 | use Doctrine\ORM\EntityManagerInterface; | ||
| 15 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 16 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 17 | use Doctrine\ORM\Mapping\DiscriminatorColumnMapping; | ||
| 18 | use Doctrine\ORM\Mapping\FieldMapping; | ||
| 19 | use Doctrine\ORM\Mapping\JoinColumnMapping; | ||
| 20 | use Doctrine\ORM\Mapping\ManyToManyOwningSideMapping; | ||
| 21 | use Doctrine\ORM\Mapping\MappingException; | ||
| 22 | use Doctrine\ORM\Mapping\QuoteStrategy; | ||
| 23 | use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; | ||
| 24 | use Doctrine\ORM\Tools\Event\GenerateSchemaTableEventArgs; | ||
| 25 | use Doctrine\ORM\Tools\Exception\MissingColumnException; | ||
| 26 | use Doctrine\ORM\Tools\Exception\NotSupported; | ||
| 27 | use Throwable; | ||
| 28 | |||
| 29 | use function array_diff; | ||
| 30 | use function array_diff_key; | ||
| 31 | use function array_filter; | ||
| 32 | use function array_flip; | ||
| 33 | use function array_intersect_key; | ||
| 34 | use function assert; | ||
| 35 | use function count; | ||
| 36 | use function current; | ||
| 37 | use function implode; | ||
| 38 | use function in_array; | ||
| 39 | use function is_numeric; | ||
| 40 | use function strtolower; | ||
| 41 | |||
| 42 | /** | ||
| 43 | * The SchemaTool is a tool to create/drop/update database schemas based on | ||
| 44 | * <tt>ClassMetadata</tt> class descriptors. | ||
| 45 | * | ||
| 46 | * @link www.doctrine-project.org | ||
| 47 | */ | ||
| 48 | class SchemaTool | ||
| 49 | { | ||
| 50 | private const KNOWN_COLUMN_OPTIONS = ['comment', 'unsigned', 'fixed', 'default']; | ||
| 51 | |||
| 52 | private readonly AbstractPlatform $platform; | ||
| 53 | private readonly QuoteStrategy $quoteStrategy; | ||
| 54 | private readonly AbstractSchemaManager $schemaManager; | ||
| 55 | |||
| 56 | /** | ||
| 57 | * Initializes a new SchemaTool instance that uses the connection of the | ||
| 58 | * provided EntityManager. | ||
| 59 | */ | ||
| 60 | public function __construct(private readonly EntityManagerInterface $em) | ||
| 61 | { | ||
| 62 | $this->platform = $em->getConnection()->getDatabasePlatform(); | ||
| 63 | $this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy(); | ||
| 64 | $this->schemaManager = $em->getConnection()->createSchemaManager(); | ||
| 65 | } | ||
| 66 | |||
| 67 | /** | ||
| 68 | * Creates the database schema for the given array of ClassMetadata instances. | ||
| 69 | * | ||
| 70 | * @psalm-param list<ClassMetadata> $classes | ||
| 71 | * | ||
| 72 | * @throws ToolsException | ||
| 73 | */ | ||
| 74 | public function createSchema(array $classes): void | ||
| 75 | { | ||
| 76 | $createSchemaSql = $this->getCreateSchemaSql($classes); | ||
| 77 | $conn = $this->em->getConnection(); | ||
| 78 | |||
| 79 | foreach ($createSchemaSql as $sql) { | ||
| 80 | try { | ||
| 81 | $conn->executeStatement($sql); | ||
| 82 | } catch (Throwable $e) { | ||
| 83 | throw ToolsException::schemaToolFailure($sql, $e); | ||
| 84 | } | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | /** | ||
| 89 | * Gets the list of DDL statements that are required to create the database schema for | ||
| 90 | * the given list of ClassMetadata instances. | ||
| 91 | * | ||
| 92 | * @psalm-param list<ClassMetadata> $classes | ||
| 93 | * | ||
| 94 | * @return list<string> The SQL statements needed to create the schema for the classes. | ||
| 95 | */ | ||
| 96 | public function getCreateSchemaSql(array $classes): array | ||
| 97 | { | ||
| 98 | $schema = $this->getSchemaFromMetadata($classes); | ||
| 99 | |||
| 100 | return $schema->toSql($this->platform); | ||
| 101 | } | ||
| 102 | |||
| 103 | /** | ||
| 104 | * Detects instances of ClassMetadata that don't need to be processed in the SchemaTool context. | ||
| 105 | * | ||
| 106 | * @psalm-param array<string, bool> $processedClasses | ||
| 107 | */ | ||
| 108 | private function processingNotRequired( | ||
| 109 | ClassMetadata $class, | ||
| 110 | array $processedClasses, | ||
| 111 | ): bool { | ||
| 112 | return isset($processedClasses[$class->name]) || | ||
| 113 | $class->isMappedSuperclass || | ||
| 114 | $class->isEmbeddedClass || | ||
| 115 | ($class->isInheritanceTypeSingleTable() && $class->name !== $class->rootEntityName) || | ||
| 116 | in_array($class->name, $this->em->getConfiguration()->getSchemaIgnoreClasses()); | ||
| 117 | } | ||
| 118 | |||
| 119 | /** | ||
| 120 | * Resolves fields in index mapping to column names | ||
| 121 | * | ||
| 122 | * @param mixed[] $indexData index or unique constraint data | ||
| 123 | * | ||
| 124 | * @return list<string> Column names from combined fields and columns mappings | ||
| 125 | */ | ||
| 126 | private function getIndexColumns(ClassMetadata $class, array $indexData): array | ||
| 127 | { | ||
| 128 | $columns = []; | ||
| 129 | |||
| 130 | if ( | ||
| 131 | isset($indexData['columns'], $indexData['fields']) | ||
| 132 | || ( | ||
| 133 | ! isset($indexData['columns']) | ||
| 134 | && ! isset($indexData['fields']) | ||
| 135 | ) | ||
| 136 | ) { | ||
| 137 | throw MappingException::invalidIndexConfiguration( | ||
| 138 | (string) $class, | ||
| 139 | $indexData['name'] ?? 'unnamed', | ||
| 140 | ); | ||
| 141 | } | ||
| 142 | |||
| 143 | if (isset($indexData['columns'])) { | ||
| 144 | $columns = $indexData['columns']; | ||
| 145 | } | ||
| 146 | |||
| 147 | if (isset($indexData['fields'])) { | ||
| 148 | foreach ($indexData['fields'] as $fieldName) { | ||
| 149 | if ($class->hasField($fieldName)) { | ||
| 150 | $columns[] = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); | ||
| 151 | } elseif ($class->hasAssociation($fieldName)) { | ||
| 152 | $assoc = $class->getAssociationMapping($fieldName); | ||
| 153 | assert($assoc->isToOneOwningSide()); | ||
| 154 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 155 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
| 156 | } | ||
| 157 | } | ||
| 158 | } | ||
| 159 | } | ||
| 160 | |||
| 161 | return $columns; | ||
| 162 | } | ||
| 163 | |||
| 164 | /** | ||
| 165 | * Creates a Schema instance from a given set of metadata classes. | ||
| 166 | * | ||
| 167 | * @psalm-param list<ClassMetadata> $classes | ||
| 168 | * | ||
| 169 | * @throws NotSupported | ||
| 170 | */ | ||
| 171 | public function getSchemaFromMetadata(array $classes): Schema | ||
| 172 | { | ||
| 173 | // Reminder for processed classes, used for hierarchies | ||
| 174 | $processedClasses = []; | ||
| 175 | $eventManager = $this->em->getEventManager(); | ||
| 176 | $metadataSchemaConfig = $this->schemaManager->createSchemaConfig(); | ||
| 177 | |||
| 178 | $schema = new Schema([], [], $metadataSchemaConfig); | ||
| 179 | |||
| 180 | $addedFks = []; | ||
| 181 | $blacklistedFks = []; | ||
| 182 | |||
| 183 | foreach ($classes as $class) { | ||
| 184 | if ($this->processingNotRequired($class, $processedClasses)) { | ||
| 185 | continue; | ||
| 186 | } | ||
| 187 | |||
| 188 | $table = $schema->createTable($this->quoteStrategy->getTableName($class, $this->platform)); | ||
| 189 | |||
| 190 | if ($class->isInheritanceTypeSingleTable()) { | ||
| 191 | $this->gatherColumns($class, $table); | ||
| 192 | $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks); | ||
| 193 | |||
| 194 | // Add the discriminator column | ||
| 195 | $this->addDiscriminatorColumnDefinition($class, $table); | ||
| 196 | |||
| 197 | // Aggregate all the information from all classes in the hierarchy | ||
| 198 | foreach ($class->parentClasses as $parentClassName) { | ||
| 199 | // Parent class information is already contained in this class | ||
| 200 | $processedClasses[$parentClassName] = true; | ||
| 201 | } | ||
| 202 | |||
| 203 | foreach ($class->subClasses as $subClassName) { | ||
| 204 | $subClass = $this->em->getClassMetadata($subClassName); | ||
| 205 | $this->gatherColumns($subClass, $table); | ||
| 206 | $this->gatherRelationsSql($subClass, $table, $schema, $addedFks, $blacklistedFks); | ||
| 207 | $processedClasses[$subClassName] = true; | ||
| 208 | } | ||
| 209 | } elseif ($class->isInheritanceTypeJoined()) { | ||
| 210 | // Add all non-inherited fields as columns | ||
| 211 | foreach ($class->fieldMappings as $fieldName => $mapping) { | ||
| 212 | if (! isset($mapping->inherited)) { | ||
| 213 | $this->gatherColumn($class, $mapping, $table); | ||
| 214 | } | ||
| 215 | } | ||
| 216 | |||
| 217 | $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks); | ||
| 218 | |||
| 219 | // Add the discriminator column only to the root table | ||
| 220 | if ($class->name === $class->rootEntityName) { | ||
| 221 | $this->addDiscriminatorColumnDefinition($class, $table); | ||
| 222 | } else { | ||
| 223 | // Add an ID FK column to child tables | ||
| 224 | $pkColumns = []; | ||
| 225 | $inheritedKeyColumns = []; | ||
| 226 | |||
| 227 | foreach ($class->identifier as $identifierField) { | ||
| 228 | if (isset($class->fieldMappings[$identifierField]->inherited)) { | ||
| 229 | $idMapping = $class->fieldMappings[$identifierField]; | ||
| 230 | $this->gatherColumn($class, $idMapping, $table); | ||
| 231 | $columnName = $this->quoteStrategy->getColumnName( | ||
| 232 | $identifierField, | ||
| 233 | $class, | ||
| 234 | $this->platform, | ||
| 235 | ); | ||
| 236 | // TODO: This seems rather hackish, can we optimize it? | ||
| 237 | $table->getColumn($columnName)->setAutoincrement(false); | ||
| 238 | |||
| 239 | $pkColumns[] = $columnName; | ||
| 240 | $inheritedKeyColumns[] = $columnName; | ||
| 241 | |||
| 242 | continue; | ||
| 243 | } | ||
| 244 | |||
| 245 | if (isset($class->associationMappings[$identifierField]->inherited)) { | ||
| 246 | $idMapping = $class->associationMappings[$identifierField]; | ||
| 247 | assert($idMapping->isToOneOwningSide()); | ||
| 248 | |||
| 249 | $targetEntity = current( | ||
| 250 | array_filter( | ||
| 251 | $classes, | ||
| 252 | static fn (ClassMetadata $class): bool => $class->name === $idMapping->targetEntity, | ||
| 253 | ), | ||
| 254 | ); | ||
| 255 | |||
| 256 | foreach ($idMapping->joinColumns as $joinColumn) { | ||
| 257 | if (isset($targetEntity->fieldMappings[$joinColumn->referencedColumnName])) { | ||
| 258 | $columnName = $this->quoteStrategy->getJoinColumnName( | ||
| 259 | $joinColumn, | ||
| 260 | $class, | ||
| 261 | $this->platform, | ||
| 262 | ); | ||
| 263 | |||
| 264 | $pkColumns[] = $columnName; | ||
| 265 | $inheritedKeyColumns[] = $columnName; | ||
| 266 | } | ||
| 267 | } | ||
| 268 | } | ||
| 269 | } | ||
| 270 | |||
| 271 | if ($inheritedKeyColumns !== []) { | ||
| 272 | // Add a FK constraint on the ID column | ||
| 273 | $table->addForeignKeyConstraint( | ||
| 274 | $this->quoteStrategy->getTableName( | ||
| 275 | $this->em->getClassMetadata($class->rootEntityName), | ||
| 276 | $this->platform, | ||
| 277 | ), | ||
| 278 | $inheritedKeyColumns, | ||
| 279 | $inheritedKeyColumns, | ||
| 280 | ['onDelete' => 'CASCADE'], | ||
| 281 | ); | ||
| 282 | } | ||
| 283 | |||
| 284 | if ($pkColumns !== []) { | ||
| 285 | $table->setPrimaryKey($pkColumns); | ||
| 286 | } | ||
| 287 | } | ||
| 288 | } else { | ||
| 289 | $this->gatherColumns($class, $table); | ||
| 290 | $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks); | ||
| 291 | } | ||
| 292 | |||
| 293 | $pkColumns = []; | ||
| 294 | |||
| 295 | foreach ($class->identifier as $identifierField) { | ||
| 296 | if (isset($class->fieldMappings[$identifierField])) { | ||
| 297 | $pkColumns[] = $this->quoteStrategy->getColumnName($identifierField, $class, $this->platform); | ||
| 298 | } elseif (isset($class->associationMappings[$identifierField])) { | ||
| 299 | $assoc = $class->associationMappings[$identifierField]; | ||
| 300 | assert($assoc->isToOneOwningSide()); | ||
| 301 | |||
| 302 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 303 | $pkColumns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
| 304 | } | ||
| 305 | } | ||
| 306 | } | ||
| 307 | |||
| 308 | if (! $table->hasIndex('primary')) { | ||
| 309 | $table->setPrimaryKey($pkColumns); | ||
| 310 | } | ||
| 311 | |||
| 312 | // there can be unique indexes automatically created for join column | ||
| 313 | // if join column is also primary key we should keep only primary key on this column | ||
| 314 | // so, remove indexes overruled by primary key | ||
| 315 | $primaryKey = $table->getIndex('primary'); | ||
| 316 | |||
| 317 | foreach ($table->getIndexes() as $idxKey => $existingIndex) { | ||
| 318 | if ($primaryKey->overrules($existingIndex)) { | ||
| 319 | $table->dropIndex($idxKey); | ||
| 320 | } | ||
| 321 | } | ||
| 322 | |||
| 323 | if (isset($class->table['indexes'])) { | ||
| 324 | foreach ($class->table['indexes'] as $indexName => $indexData) { | ||
| 325 | if (! isset($indexData['flags'])) { | ||
| 326 | $indexData['flags'] = []; | ||
| 327 | } | ||
| 328 | |||
| 329 | $table->addIndex( | ||
| 330 | $this->getIndexColumns($class, $indexData), | ||
| 331 | is_numeric($indexName) ? null : $indexName, | ||
| 332 | (array) $indexData['flags'], | ||
| 333 | $indexData['options'] ?? [], | ||
| 334 | ); | ||
| 335 | } | ||
| 336 | } | ||
| 337 | |||
| 338 | if (isset($class->table['uniqueConstraints'])) { | ||
| 339 | foreach ($class->table['uniqueConstraints'] as $indexName => $indexData) { | ||
| 340 | $uniqIndex = new Index('tmp__' . $indexName, $this->getIndexColumns($class, $indexData), true, false, [], $indexData['options'] ?? []); | ||
| 341 | |||
| 342 | foreach ($table->getIndexes() as $tableIndexName => $tableIndex) { | ||
| 343 | if ($tableIndex->isFulfilledBy($uniqIndex)) { | ||
| 344 | $table->dropIndex($tableIndexName); | ||
| 345 | break; | ||
| 346 | } | ||
| 347 | } | ||
| 348 | |||
| 349 | $table->addUniqueIndex($uniqIndex->getColumns(), is_numeric($indexName) ? null : $indexName, $indexData['options'] ?? []); | ||
| 350 | } | ||
| 351 | } | ||
| 352 | |||
| 353 | if (isset($class->table['options'])) { | ||
| 354 | foreach ($class->table['options'] as $key => $val) { | ||
| 355 | $table->addOption($key, $val); | ||
| 356 | } | ||
| 357 | } | ||
| 358 | |||
| 359 | $processedClasses[$class->name] = true; | ||
| 360 | |||
| 361 | if ($class->isIdGeneratorSequence() && $class->name === $class->rootEntityName) { | ||
| 362 | $seqDef = $class->sequenceGeneratorDefinition; | ||
| 363 | $quotedName = $this->quoteStrategy->getSequenceName($seqDef, $class, $this->platform); | ||
| 364 | if (! $schema->hasSequence($quotedName)) { | ||
| 365 | $schema->createSequence( | ||
| 366 | $quotedName, | ||
| 367 | (int) $seqDef['allocationSize'], | ||
| 368 | (int) $seqDef['initialValue'], | ||
| 369 | ); | ||
| 370 | } | ||
| 371 | } | ||
| 372 | |||
| 373 | if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) { | ||
| 374 | $eventManager->dispatchEvent( | ||
| 375 | ToolEvents::postGenerateSchemaTable, | ||
| 376 | new GenerateSchemaTableEventArgs($class, $schema, $table), | ||
| 377 | ); | ||
| 378 | } | ||
| 379 | } | ||
| 380 | |||
| 381 | if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) { | ||
| 382 | $eventManager->dispatchEvent( | ||
| 383 | ToolEvents::postGenerateSchema, | ||
| 384 | new GenerateSchemaEventArgs($this->em, $schema), | ||
| 385 | ); | ||
| 386 | } | ||
| 387 | |||
| 388 | return $schema; | ||
| 389 | } | ||
| 390 | |||
| 391 | /** | ||
| 392 | * Gets a portable column definition as required by the DBAL for the discriminator | ||
| 393 | * column of a class. | ||
| 394 | */ | ||
| 395 | private function addDiscriminatorColumnDefinition(ClassMetadata $class, Table $table): void | ||
| 396 | { | ||
| 397 | $discrColumn = $class->discriminatorColumn; | ||
| 398 | assert($discrColumn !== null); | ||
| 399 | |||
| 400 | if (strtolower($discrColumn->type) === 'string' && ! isset($discrColumn->length)) { | ||
| 401 | $discrColumn->type = 'string'; | ||
| 402 | $discrColumn->length = 255; | ||
| 403 | } | ||
| 404 | |||
| 405 | $options = [ | ||
| 406 | 'length' => $discrColumn->length ?? null, | ||
| 407 | 'notnull' => true, | ||
| 408 | ]; | ||
| 409 | |||
| 410 | if (isset($discrColumn->columnDefinition)) { | ||
| 411 | $options['columnDefinition'] = $discrColumn->columnDefinition; | ||
| 412 | } | ||
| 413 | |||
| 414 | $options = $this->gatherColumnOptions($discrColumn) + $options; | ||
| 415 | $table->addColumn($discrColumn->name, $discrColumn->type, $options); | ||
| 416 | } | ||
| 417 | |||
| 418 | /** | ||
| 419 | * Gathers the column definitions as required by the DBAL of all field mappings | ||
| 420 | * found in the given class. | ||
| 421 | */ | ||
| 422 | private function gatherColumns(ClassMetadata $class, Table $table): void | ||
| 423 | { | ||
| 424 | $pkColumns = []; | ||
| 425 | |||
| 426 | foreach ($class->fieldMappings as $mapping) { | ||
| 427 | if ($class->isInheritanceTypeSingleTable() && isset($mapping->inherited)) { | ||
| 428 | continue; | ||
| 429 | } | ||
| 430 | |||
| 431 | $this->gatherColumn($class, $mapping, $table); | ||
| 432 | |||
| 433 | if ($class->isIdentifier($mapping->fieldName)) { | ||
| 434 | $pkColumns[] = $this->quoteStrategy->getColumnName($mapping->fieldName, $class, $this->platform); | ||
| 435 | } | ||
| 436 | } | ||
| 437 | } | ||
| 438 | |||
| 439 | /** | ||
| 440 | * Creates a column definition as required by the DBAL from an ORM field mapping definition. | ||
| 441 | * | ||
| 442 | * @param ClassMetadata $class The class that owns the field mapping. | ||
| 443 | * @psalm-param FieldMapping $mapping The field mapping. | ||
| 444 | */ | ||
| 445 | private function gatherColumn( | ||
| 446 | ClassMetadata $class, | ||
| 447 | FieldMapping $mapping, | ||
| 448 | Table $table, | ||
| 449 | ): void { | ||
| 450 | $columnName = $this->quoteStrategy->getColumnName($mapping->fieldName, $class, $this->platform); | ||
| 451 | $columnType = $mapping->type; | ||
| 452 | |||
| 453 | $options = []; | ||
| 454 | $options['length'] = $mapping->length ?? null; | ||
| 455 | $options['notnull'] = isset($mapping->nullable) ? ! $mapping->nullable : true; | ||
| 456 | if ($class->isInheritanceTypeSingleTable() && $class->parentClasses) { | ||
| 457 | $options['notnull'] = false; | ||
| 458 | } | ||
| 459 | |||
| 460 | $options['platformOptions'] = []; | ||
| 461 | $options['platformOptions']['version'] = $class->isVersioned && $class->versionField === $mapping->fieldName; | ||
| 462 | |||
| 463 | if (strtolower($columnType) === 'string' && $options['length'] === null) { | ||
| 464 | $options['length'] = 255; | ||
| 465 | } | ||
| 466 | |||
| 467 | if (isset($mapping->precision)) { | ||
| 468 | $options['precision'] = $mapping->precision; | ||
| 469 | } | ||
| 470 | |||
| 471 | if (isset($mapping->scale)) { | ||
| 472 | $options['scale'] = $mapping->scale; | ||
| 473 | } | ||
| 474 | |||
| 475 | if (isset($mapping->default)) { | ||
| 476 | $options['default'] = $mapping->default; | ||
| 477 | } | ||
| 478 | |||
| 479 | if (isset($mapping->columnDefinition)) { | ||
| 480 | $options['columnDefinition'] = $mapping->columnDefinition; | ||
| 481 | } | ||
| 482 | |||
| 483 | // the 'default' option can be overwritten here | ||
| 484 | $options = $this->gatherColumnOptions($mapping) + $options; | ||
| 485 | |||
| 486 | if ($class->isIdGeneratorIdentity() && $class->getIdentifierFieldNames() === [$mapping->fieldName]) { | ||
| 487 | $options['autoincrement'] = true; | ||
| 488 | } | ||
| 489 | |||
| 490 | if ($class->isInheritanceTypeJoined() && $class->name !== $class->rootEntityName) { | ||
| 491 | $options['autoincrement'] = false; | ||
| 492 | } | ||
| 493 | |||
| 494 | if ($table->hasColumn($columnName)) { | ||
| 495 | // required in some inheritance scenarios | ||
| 496 | $table->modifyColumn($columnName, $options); | ||
| 497 | } else { | ||
| 498 | $table->addColumn($columnName, $columnType, $options); | ||
| 499 | } | ||
| 500 | |||
| 501 | $isUnique = $mapping->unique ?? false; | ||
| 502 | if ($isUnique) { | ||
| 503 | $table->addUniqueIndex([$columnName]); | ||
| 504 | } | ||
| 505 | } | ||
| 506 | |||
| 507 | /** | ||
| 508 | * Gathers the SQL for properly setting up the relations of the given class. | ||
| 509 | * This includes the SQL for foreign key constraints and join tables. | ||
| 510 | * | ||
| 511 | * @psalm-param array<string, array{ | ||
| 512 | * foreignTableName: string, | ||
| 513 | * foreignColumns: list<string> | ||
| 514 | * }> $addedFks | ||
| 515 | * @psalm-param array<string, bool> $blacklistedFks | ||
| 516 | * | ||
| 517 | * @throws NotSupported | ||
| 518 | */ | ||
| 519 | private function gatherRelationsSql( | ||
| 520 | ClassMetadata $class, | ||
| 521 | Table $table, | ||
| 522 | Schema $schema, | ||
| 523 | array &$addedFks, | ||
| 524 | array &$blacklistedFks, | ||
| 525 | ): void { | ||
| 526 | foreach ($class->associationMappings as $id => $mapping) { | ||
| 527 | if (isset($mapping->inherited) && ! in_array($id, $class->identifier, true)) { | ||
| 528 | continue; | ||
| 529 | } | ||
| 530 | |||
| 531 | $foreignClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 532 | |||
| 533 | if ($mapping->isToOneOwningSide()) { | ||
| 534 | $primaryKeyColumns = []; // PK is unnecessary for this relation-type | ||
| 535 | |||
| 536 | $this->gatherRelationJoinColumns( | ||
| 537 | $mapping->joinColumns, | ||
| 538 | $table, | ||
| 539 | $foreignClass, | ||
| 540 | $mapping, | ||
| 541 | $primaryKeyColumns, | ||
| 542 | $addedFks, | ||
| 543 | $blacklistedFks, | ||
| 544 | ); | ||
| 545 | } elseif ($mapping instanceof ManyToManyOwningSideMapping) { | ||
| 546 | // create join table | ||
| 547 | $joinTable = $mapping->joinTable; | ||
| 548 | |||
| 549 | $theJoinTable = $schema->createTable( | ||
| 550 | $this->quoteStrategy->getJoinTableName($mapping, $foreignClass, $this->platform), | ||
| 551 | ); | ||
| 552 | |||
| 553 | foreach ($joinTable->options as $key => $val) { | ||
| 554 | $theJoinTable->addOption($key, $val); | ||
| 555 | } | ||
| 556 | |||
| 557 | $primaryKeyColumns = []; | ||
| 558 | |||
| 559 | // Build first FK constraint (relation table => source table) | ||
| 560 | $this->gatherRelationJoinColumns( | ||
| 561 | $joinTable->joinColumns, | ||
| 562 | $theJoinTable, | ||
| 563 | $class, | ||
| 564 | $mapping, | ||
| 565 | $primaryKeyColumns, | ||
| 566 | $addedFks, | ||
| 567 | $blacklistedFks, | ||
| 568 | ); | ||
| 569 | |||
| 570 | // Build second FK constraint (relation table => target table) | ||
| 571 | $this->gatherRelationJoinColumns( | ||
| 572 | $joinTable->inverseJoinColumns, | ||
| 573 | $theJoinTable, | ||
| 574 | $foreignClass, | ||
| 575 | $mapping, | ||
| 576 | $primaryKeyColumns, | ||
| 577 | $addedFks, | ||
| 578 | $blacklistedFks, | ||
| 579 | ); | ||
| 580 | |||
| 581 | $theJoinTable->setPrimaryKey($primaryKeyColumns); | ||
| 582 | } | ||
| 583 | } | ||
| 584 | } | ||
| 585 | |||
| 586 | /** | ||
| 587 | * Gets the class metadata that is responsible for the definition of the referenced column name. | ||
| 588 | * | ||
| 589 | * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its | ||
| 590 | * not a simple field, go through all identifier field names that are associations recursively and | ||
| 591 | * find that referenced column name. | ||
| 592 | * | ||
| 593 | * TODO: Is there any way to make this code more pleasing? | ||
| 594 | * | ||
| 595 | * @psalm-return array{ClassMetadata, string}|null | ||
| 596 | */ | ||
| 597 | private function getDefiningClass(ClassMetadata $class, string $referencedColumnName): array|null | ||
| 598 | { | ||
| 599 | $referencedFieldName = $class->getFieldName($referencedColumnName); | ||
| 600 | |||
| 601 | if ($class->hasField($referencedFieldName)) { | ||
| 602 | return [$class, $referencedFieldName]; | ||
| 603 | } | ||
| 604 | |||
| 605 | if (in_array($referencedColumnName, $class->getIdentifierColumnNames(), true)) { | ||
| 606 | // it seems to be an entity as foreign key | ||
| 607 | foreach ($class->getIdentifierFieldNames() as $fieldName) { | ||
| 608 | if ( | ||
| 609 | $class->hasAssociation($fieldName) | ||
| 610 | && $class->getSingleAssociationJoinColumnName($fieldName) === $referencedColumnName | ||
| 611 | ) { | ||
| 612 | return $this->getDefiningClass( | ||
| 613 | $this->em->getClassMetadata($class->associationMappings[$fieldName]->targetEntity), | ||
| 614 | $class->getSingleAssociationReferencedJoinColumnName($fieldName), | ||
| 615 | ); | ||
| 616 | } | ||
| 617 | } | ||
| 618 | } | ||
| 619 | |||
| 620 | return null; | ||
| 621 | } | ||
| 622 | |||
| 623 | /** | ||
| 624 | * Gathers columns and fk constraints that are required for one part of relationship. | ||
| 625 | * | ||
| 626 | * @psalm-param list<JoinColumnMapping> $joinColumns | ||
| 627 | * @psalm-param list<string> $primaryKeyColumns | ||
| 628 | * @psalm-param array<string, array{ | ||
| 629 | * foreignTableName: string, | ||
| 630 | * foreignColumns: list<string> | ||
| 631 | * }> $addedFks | ||
| 632 | * @psalm-param array<string,bool> $blacklistedFks | ||
| 633 | * | ||
| 634 | * @throws MissingColumnException | ||
| 635 | */ | ||
| 636 | private function gatherRelationJoinColumns( | ||
| 637 | array $joinColumns, | ||
| 638 | Table $theJoinTable, | ||
| 639 | ClassMetadata $class, | ||
| 640 | AssociationMapping $mapping, | ||
| 641 | array &$primaryKeyColumns, | ||
| 642 | array &$addedFks, | ||
| 643 | array &$blacklistedFks, | ||
| 644 | ): void { | ||
| 645 | $localColumns = []; | ||
| 646 | $foreignColumns = []; | ||
| 647 | $fkOptions = []; | ||
| 648 | $foreignTableName = $this->quoteStrategy->getTableName($class, $this->platform); | ||
| 649 | $uniqueConstraints = []; | ||
| 650 | |||
| 651 | foreach ($joinColumns as $joinColumn) { | ||
| 652 | [$definingClass, $referencedFieldName] = $this->getDefiningClass( | ||
| 653 | $class, | ||
| 654 | $joinColumn->referencedColumnName, | ||
| 655 | ); | ||
| 656 | |||
| 657 | if (! $definingClass) { | ||
| 658 | throw MissingColumnException::fromColumnSourceAndTarget( | ||
| 659 | $joinColumn->referencedColumnName, | ||
| 660 | $mapping->sourceEntity, | ||
| 661 | $mapping->targetEntity, | ||
| 662 | ); | ||
| 663 | } | ||
| 664 | |||
| 665 | $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
| 666 | $quotedRefColumnName = $this->quoteStrategy->getReferencedJoinColumnName( | ||
| 667 | $joinColumn, | ||
| 668 | $class, | ||
| 669 | $this->platform, | ||
| 670 | ); | ||
| 671 | |||
| 672 | $primaryKeyColumns[] = $quotedColumnName; | ||
| 673 | $localColumns[] = $quotedColumnName; | ||
| 674 | $foreignColumns[] = $quotedRefColumnName; | ||
| 675 | |||
| 676 | if (! $theJoinTable->hasColumn($quotedColumnName)) { | ||
| 677 | // Only add the column to the table if it does not exist already. | ||
| 678 | // It might exist already if the foreign key is mapped into a regular | ||
| 679 | // property as well. | ||
| 680 | |||
| 681 | $fieldMapping = $definingClass->getFieldMapping($referencedFieldName); | ||
| 682 | |||
| 683 | $columnOptions = ['notnull' => false]; | ||
| 684 | |||
| 685 | if (isset($joinColumn->columnDefinition)) { | ||
| 686 | $columnOptions['columnDefinition'] = $joinColumn->columnDefinition; | ||
| 687 | } elseif (isset($fieldMapping->columnDefinition)) { | ||
| 688 | $columnOptions['columnDefinition'] = $fieldMapping->columnDefinition; | ||
| 689 | } | ||
| 690 | |||
| 691 | if (isset($joinColumn->nullable)) { | ||
| 692 | $columnOptions['notnull'] = ! $joinColumn->nullable; | ||
| 693 | } | ||
| 694 | |||
| 695 | $columnOptions += $this->gatherColumnOptions($fieldMapping); | ||
| 696 | |||
| 697 | if (isset($fieldMapping->length)) { | ||
| 698 | $columnOptions['length'] = $fieldMapping->length; | ||
| 699 | } | ||
| 700 | |||
| 701 | if ($fieldMapping->type === 'decimal') { | ||
| 702 | $columnOptions['scale'] = $fieldMapping->scale; | ||
| 703 | $columnOptions['precision'] = $fieldMapping->precision; | ||
| 704 | } | ||
| 705 | |||
| 706 | $columnOptions = $this->gatherColumnOptions($joinColumn) + $columnOptions; | ||
| 707 | |||
| 708 | $theJoinTable->addColumn($quotedColumnName, $fieldMapping->type, $columnOptions); | ||
| 709 | } | ||
| 710 | |||
| 711 | if (isset($joinColumn->unique) && $joinColumn->unique === true) { | ||
| 712 | $uniqueConstraints[] = ['columns' => [$quotedColumnName]]; | ||
| 713 | } | ||
| 714 | |||
| 715 | if (isset($joinColumn->onDelete)) { | ||
| 716 | $fkOptions['onDelete'] = $joinColumn->onDelete; | ||
| 717 | } | ||
| 718 | } | ||
| 719 | |||
| 720 | // Prefer unique constraints over implicit simple indexes created for foreign keys. | ||
| 721 | // Also avoids index duplication. | ||
| 722 | foreach ($uniqueConstraints as $indexName => $unique) { | ||
| 723 | $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName); | ||
| 724 | } | ||
| 725 | |||
| 726 | $compositeName = $theJoinTable->getName() . '.' . implode('', $localColumns); | ||
| 727 | if ( | ||
| 728 | isset($addedFks[$compositeName]) | ||
| 729 | && ($foreignTableName !== $addedFks[$compositeName]['foreignTableName'] | ||
| 730 | || 0 < count(array_diff($foreignColumns, $addedFks[$compositeName]['foreignColumns']))) | ||
| 731 | ) { | ||
| 732 | foreach ($theJoinTable->getForeignKeys() as $fkName => $key) { | ||
| 733 | if ( | ||
| 734 | count(array_diff($key->getLocalColumns(), $localColumns)) === 0 | ||
| 735 | && (($key->getForeignTableName() !== $foreignTableName) | ||
| 736 | || 0 < count(array_diff($key->getForeignColumns(), $foreignColumns))) | ||
| 737 | ) { | ||
| 738 | $theJoinTable->removeForeignKey($fkName); | ||
| 739 | break; | ||
| 740 | } | ||
| 741 | } | ||
| 742 | |||
| 743 | $blacklistedFks[$compositeName] = true; | ||
| 744 | } elseif (! isset($blacklistedFks[$compositeName])) { | ||
| 745 | $addedFks[$compositeName] = ['foreignTableName' => $foreignTableName, 'foreignColumns' => $foreignColumns]; | ||
| 746 | $theJoinTable->addForeignKeyConstraint( | ||
| 747 | $foreignTableName, | ||
| 748 | $localColumns, | ||
| 749 | $foreignColumns, | ||
| 750 | $fkOptions, | ||
| 751 | ); | ||
| 752 | } | ||
| 753 | } | ||
| 754 | |||
| 755 | /** @return mixed[] */ | ||
| 756 | private function gatherColumnOptions(JoinColumnMapping|FieldMapping|DiscriminatorColumnMapping $mapping): array | ||
| 757 | { | ||
| 758 | $mappingOptions = $mapping->options ?? []; | ||
| 759 | |||
| 760 | if (isset($mapping->enumType)) { | ||
| 761 | $mappingOptions['enumType'] = $mapping->enumType; | ||
| 762 | } | ||
| 763 | |||
| 764 | if (($mappingOptions['default'] ?? null) instanceof BackedEnum) { | ||
| 765 | $mappingOptions['default'] = $mappingOptions['default']->value; | ||
| 766 | } | ||
| 767 | |||
| 768 | if (empty($mappingOptions)) { | ||
| 769 | return []; | ||
| 770 | } | ||
| 771 | |||
| 772 | $options = array_intersect_key($mappingOptions, array_flip(self::KNOWN_COLUMN_OPTIONS)); | ||
| 773 | $options['platformOptions'] = array_diff_key($mappingOptions, $options); | ||
| 774 | |||
| 775 | return $options; | ||
| 776 | } | ||
| 777 | |||
| 778 | /** | ||
| 779 | * Drops the database schema for the given classes. | ||
| 780 | * | ||
| 781 | * In any way when an exception is thrown it is suppressed since drop was | ||
| 782 | * issued for all classes of the schema and some probably just don't exist. | ||
| 783 | * | ||
| 784 | * @psalm-param list<ClassMetadata> $classes | ||
| 785 | */ | ||
| 786 | public function dropSchema(array $classes): void | ||
| 787 | { | ||
| 788 | $dropSchemaSql = $this->getDropSchemaSQL($classes); | ||
| 789 | $conn = $this->em->getConnection(); | ||
| 790 | |||
| 791 | foreach ($dropSchemaSql as $sql) { | ||
| 792 | try { | ||
| 793 | $conn->executeStatement($sql); | ||
| 794 | } catch (Throwable) { | ||
| 795 | // ignored | ||
| 796 | } | ||
| 797 | } | ||
| 798 | } | ||
| 799 | |||
| 800 | /** | ||
| 801 | * Drops all elements in the database of the current connection. | ||
| 802 | */ | ||
| 803 | public function dropDatabase(): void | ||
| 804 | { | ||
| 805 | $dropSchemaSql = $this->getDropDatabaseSQL(); | ||
| 806 | $conn = $this->em->getConnection(); | ||
| 807 | |||
| 808 | foreach ($dropSchemaSql as $sql) { | ||
| 809 | $conn->executeStatement($sql); | ||
| 810 | } | ||
| 811 | } | ||
| 812 | |||
| 813 | /** | ||
| 814 | * Gets the SQL needed to drop the database schema for the connections database. | ||
| 815 | * | ||
| 816 | * @return list<string> | ||
| 817 | */ | ||
| 818 | public function getDropDatabaseSQL(): array | ||
| 819 | { | ||
| 820 | return $this->schemaManager | ||
| 821 | ->introspectSchema() | ||
| 822 | ->toDropSql($this->platform); | ||
| 823 | } | ||
| 824 | |||
| 825 | /** | ||
| 826 | * Gets SQL to drop the tables defined by the passed classes. | ||
| 827 | * | ||
| 828 | * @psalm-param list<ClassMetadata> $classes | ||
| 829 | * | ||
| 830 | * @return list<string> | ||
| 831 | */ | ||
| 832 | public function getDropSchemaSQL(array $classes): array | ||
| 833 | { | ||
| 834 | $schema = $this->getSchemaFromMetadata($classes); | ||
| 835 | |||
| 836 | $deployedSchema = $this->schemaManager->introspectSchema(); | ||
| 837 | |||
| 838 | foreach ($schema->getTables() as $table) { | ||
| 839 | if (! $deployedSchema->hasTable($table->getName())) { | ||
| 840 | $schema->dropTable($table->getName()); | ||
| 841 | } | ||
| 842 | } | ||
| 843 | |||
| 844 | if ($this->platform->supportsSequences()) { | ||
| 845 | foreach ($schema->getSequences() as $sequence) { | ||
| 846 | if (! $deployedSchema->hasSequence($sequence->getName())) { | ||
| 847 | $schema->dropSequence($sequence->getName()); | ||
| 848 | } | ||
| 849 | } | ||
| 850 | |||
| 851 | foreach ($schema->getTables() as $table) { | ||
| 852 | $primaryKey = $table->getPrimaryKey(); | ||
| 853 | if ($primaryKey === null) { | ||
| 854 | continue; | ||
| 855 | } | ||
| 856 | |||
| 857 | $columns = $primaryKey->getColumns(); | ||
| 858 | if (count($columns) === 1) { | ||
| 859 | $checkSequence = $table->getName() . '_' . $columns[0] . '_seq'; | ||
| 860 | if ($deployedSchema->hasSequence($checkSequence) && ! $schema->hasSequence($checkSequence)) { | ||
| 861 | $schema->createSequence($checkSequence); | ||
| 862 | } | ||
| 863 | } | ||
| 864 | } | ||
| 865 | } | ||
| 866 | |||
| 867 | return $schema->toDropSql($this->platform); | ||
| 868 | } | ||
| 869 | |||
| 870 | /** | ||
| 871 | * Updates the database schema of the given classes by comparing the ClassMetadata | ||
| 872 | * instances to the current database schema that is inspected. | ||
| 873 | * | ||
| 874 | * @param mixed[] $classes | ||
| 875 | */ | ||
| 876 | public function updateSchema(array $classes): void | ||
| 877 | { | ||
| 878 | $conn = $this->em->getConnection(); | ||
| 879 | |||
| 880 | foreach ($this->getUpdateSchemaSql($classes) as $sql) { | ||
| 881 | $conn->executeStatement($sql); | ||
| 882 | } | ||
| 883 | } | ||
| 884 | |||
| 885 | /** | ||
| 886 | * Gets the sequence of SQL statements that need to be performed in order | ||
| 887 | * to bring the given class mappings in-synch with the relational schema. | ||
| 888 | * | ||
| 889 | * @param list<ClassMetadata> $classes The classes to consider. | ||
| 890 | * | ||
| 891 | * @return list<string> The sequence of SQL statements. | ||
| 892 | */ | ||
| 893 | public function getUpdateSchemaSql(array $classes): array | ||
| 894 | { | ||
| 895 | $toSchema = $this->getSchemaFromMetadata($classes); | ||
| 896 | $fromSchema = $this->createSchemaForComparison($toSchema); | ||
| 897 | $comparator = $this->schemaManager->createComparator(); | ||
| 898 | $schemaDiff = $comparator->compareSchemas($fromSchema, $toSchema); | ||
| 899 | |||
| 900 | return $this->platform->getAlterSchemaSQL($schemaDiff); | ||
| 901 | } | ||
| 902 | |||
| 903 | /** | ||
| 904 | * Creates the schema from the database, ensuring tables from the target schema are whitelisted for comparison. | ||
| 905 | */ | ||
| 906 | private function createSchemaForComparison(Schema $toSchema): Schema | ||
| 907 | { | ||
| 908 | $connection = $this->em->getConnection(); | ||
| 909 | |||
| 910 | // backup schema assets filter | ||
| 911 | $config = $connection->getConfiguration(); | ||
| 912 | $previousFilter = $config->getSchemaAssetsFilter(); | ||
| 913 | |||
| 914 | if ($previousFilter === null) { | ||
| 915 | return $this->schemaManager->introspectSchema(); | ||
| 916 | } | ||
| 917 | |||
| 918 | // whitelist assets we already know about in $toSchema, use the existing filter otherwise | ||
| 919 | $config->setSchemaAssetsFilter(static function ($asset) use ($previousFilter, $toSchema): bool { | ||
| 920 | $assetName = $asset instanceof AbstractAsset ? $asset->getName() : $asset; | ||
| 921 | |||
| 922 | return $toSchema->hasTable($assetName) || $toSchema->hasSequence($assetName) || $previousFilter($asset); | ||
| 923 | }); | ||
| 924 | |||
| 925 | try { | ||
| 926 | return $this->schemaManager->introspectSchema(); | ||
| 927 | } finally { | ||
| 928 | // restore schema assets filter | ||
| 929 | $config->setSchemaAssetsFilter($previousFilter); | ||
| 930 | } | ||
| 931 | } | ||
| 932 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools; | ||
| 6 | |||
| 7 | use BackedEnum; | ||
| 8 | use Doctrine\DBAL\Types\AsciiStringType; | ||
| 9 | use Doctrine\DBAL\Types\BigIntType; | ||
| 10 | use Doctrine\DBAL\Types\BooleanType; | ||
| 11 | use Doctrine\DBAL\Types\DecimalType; | ||
| 12 | use Doctrine\DBAL\Types\FloatType; | ||
| 13 | use Doctrine\DBAL\Types\GuidType; | ||
| 14 | use Doctrine\DBAL\Types\IntegerType; | ||
| 15 | use Doctrine\DBAL\Types\JsonType; | ||
| 16 | use Doctrine\DBAL\Types\SimpleArrayType; | ||
| 17 | use Doctrine\DBAL\Types\SmallIntType; | ||
| 18 | use Doctrine\DBAL\Types\StringType; | ||
| 19 | use Doctrine\DBAL\Types\TextType; | ||
| 20 | use Doctrine\DBAL\Types\Type; | ||
| 21 | use Doctrine\ORM\EntityManagerInterface; | ||
| 22 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 23 | use Doctrine\ORM\Mapping\FieldMapping; | ||
| 24 | use ReflectionEnum; | ||
| 25 | use ReflectionNamedType; | ||
| 26 | |||
| 27 | use function array_diff; | ||
| 28 | use function array_filter; | ||
| 29 | use function array_key_exists; | ||
| 30 | use function array_map; | ||
| 31 | use function array_push; | ||
| 32 | use function array_search; | ||
| 33 | use function array_values; | ||
| 34 | use function assert; | ||
| 35 | use function class_exists; | ||
| 36 | use function class_parents; | ||
| 37 | use function count; | ||
| 38 | use function implode; | ||
| 39 | use function in_array; | ||
| 40 | use function interface_exists; | ||
| 41 | use function is_a; | ||
| 42 | use function sprintf; | ||
| 43 | |||
| 44 | /** | ||
| 45 | * Performs strict validation of the mapping schema | ||
| 46 | * | ||
| 47 | * @link www.doctrine-project.com | ||
| 48 | */ | ||
| 49 | class SchemaValidator | ||
| 50 | { | ||
| 51 | /** | ||
| 52 | * It maps built-in Doctrine types to PHP types | ||
| 53 | */ | ||
| 54 | private const BUILTIN_TYPES_MAP = [ | ||
| 55 | AsciiStringType::class => ['string'], | ||
| 56 | BigIntType::class => ['int', 'string'], | ||
| 57 | BooleanType::class => ['bool'], | ||
| 58 | DecimalType::class => ['string'], | ||
| 59 | FloatType::class => ['float'], | ||
| 60 | GuidType::class => ['string'], | ||
| 61 | IntegerType::class => ['int'], | ||
| 62 | JsonType::class => ['array'], | ||
| 63 | SimpleArrayType::class => ['array'], | ||
| 64 | SmallIntType::class => ['int'], | ||
| 65 | StringType::class => ['string'], | ||
| 66 | TextType::class => ['string'], | ||
| 67 | ]; | ||
| 68 | |||
| 69 | public function __construct( | ||
| 70 | private readonly EntityManagerInterface $em, | ||
| 71 | private readonly bool $validatePropertyTypes = true, | ||
| 72 | ) { | ||
| 73 | } | ||
| 74 | |||
| 75 | /** | ||
| 76 | * Checks the internal consistency of all mapping files. | ||
| 77 | * | ||
| 78 | * There are several checks that can't be done at runtime or are too expensive, which can be verified | ||
| 79 | * with this command. For example: | ||
| 80 | * | ||
| 81 | * 1. Check if a relation with "mappedBy" is actually connected to that specified field. | ||
| 82 | * 2. Check if "mappedBy" and "inversedBy" are consistent to each other. | ||
| 83 | * 3. Check if "referencedColumnName" attributes are really pointing to primary key columns. | ||
| 84 | * | ||
| 85 | * @psalm-return array<string, list<string>> | ||
| 86 | */ | ||
| 87 | public function validateMapping(): array | ||
| 88 | { | ||
| 89 | $errors = []; | ||
| 90 | $cmf = $this->em->getMetadataFactory(); | ||
| 91 | $classes = $cmf->getAllMetadata(); | ||
| 92 | |||
| 93 | foreach ($classes as $class) { | ||
| 94 | $ce = $this->validateClass($class); | ||
| 95 | if ($ce) { | ||
| 96 | $errors[$class->name] = $ce; | ||
| 97 | } | ||
| 98 | } | ||
| 99 | |||
| 100 | return $errors; | ||
| 101 | } | ||
| 102 | |||
| 103 | /** | ||
| 104 | * Validates a single class of the current. | ||
| 105 | * | ||
| 106 | * @return string[] | ||
| 107 | * @psalm-return list<string> | ||
| 108 | */ | ||
| 109 | public function validateClass(ClassMetadata $class): array | ||
| 110 | { | ||
| 111 | $ce = []; | ||
| 112 | $cmf = $this->em->getMetadataFactory(); | ||
| 113 | |||
| 114 | foreach ($class->fieldMappings as $fieldName => $mapping) { | ||
| 115 | if (! Type::hasType($mapping->type)) { | ||
| 116 | $ce[] = "The field '" . $class->name . '#' . $fieldName . "' uses a non-existent type '" . $mapping->type . "'."; | ||
| 117 | } | ||
| 118 | } | ||
| 119 | |||
| 120 | if ($this->validatePropertyTypes) { | ||
| 121 | array_push($ce, ...$this->validatePropertiesTypes($class)); | ||
| 122 | } | ||
| 123 | |||
| 124 | foreach ($class->associationMappings as $fieldName => $assoc) { | ||
| 125 | if (! class_exists($assoc->targetEntity) || $cmf->isTransient($assoc->targetEntity)) { | ||
| 126 | $ce[] = "The target entity '" . $assoc->targetEntity . "' specified on " . $class->name . '#' . $fieldName . ' is unknown or not an entity.'; | ||
| 127 | |||
| 128 | return $ce; | ||
| 129 | } | ||
| 130 | |||
| 131 | $targetMetadata = $cmf->getMetadataFor($assoc->targetEntity); | ||
| 132 | |||
| 133 | if ($targetMetadata->isMappedSuperclass) { | ||
| 134 | $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.'; | ||
| 135 | |||
| 136 | return $ce; | ||
| 137 | } | ||
| 138 | |||
| 139 | if (isset($assoc->id) && $targetMetadata->containsForeignIdentifier) { | ||
| 140 | $ce[] = "Cannot map association '" . $class->name . '#' . $fieldName . ' as identifier, because ' . | ||
| 141 | "the target entity '" . $targetMetadata->name . "' also maps an association as identifier."; | ||
| 142 | } | ||
| 143 | |||
| 144 | if (! $assoc->isOwningSide()) { | ||
| 145 | if ($targetMetadata->hasField($assoc->mappedBy)) { | ||
| 146 | $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the owning side ' . | ||
| 147 | 'field ' . $assoc->targetEntity . '#' . $assoc->mappedBy . ' which is not defined as association, but as field.'; | ||
| 148 | } | ||
| 149 | |||
| 150 | if (! $targetMetadata->hasAssociation($assoc->mappedBy)) { | ||
| 151 | $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the owning side ' . | ||
| 152 | 'field ' . $assoc->targetEntity . '#' . $assoc->mappedBy . ' which does not exist.'; | ||
| 153 | } elseif ($targetMetadata->associationMappings[$assoc->mappedBy]->inversedBy === null) { | ||
| 154 | $ce[] = 'The field ' . $class->name . '#' . $fieldName . ' is on the inverse side of a ' . | ||
| 155 | 'bi-directional relationship, but the specified mappedBy association on the target-entity ' . | ||
| 156 | $assoc->targetEntity . '#' . $assoc->mappedBy . ' does not contain the required ' . | ||
| 157 | "'inversedBy=\"" . $fieldName . "\"' attribute."; | ||
| 158 | } elseif ($targetMetadata->associationMappings[$assoc->mappedBy]->inversedBy !== $fieldName) { | ||
| 159 | $ce[] = 'The mappings ' . $class->name . '#' . $fieldName . ' and ' . | ||
| 160 | $assoc->targetEntity . '#' . $assoc->mappedBy . ' are ' . | ||
| 161 | 'inconsistent with each other.'; | ||
| 162 | } | ||
| 163 | } | ||
| 164 | |||
| 165 | if ($assoc->isOwningSide() && $assoc->inversedBy !== null) { | ||
| 166 | if ($targetMetadata->hasField($assoc->inversedBy)) { | ||
| 167 | $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the inverse side ' . | ||
| 168 | 'field ' . $assoc->targetEntity . '#' . $assoc->inversedBy . ' which is not defined as association.'; | ||
| 169 | } | ||
| 170 | |||
| 171 | if (! $targetMetadata->hasAssociation($assoc->inversedBy)) { | ||
| 172 | $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the inverse side ' . | ||
| 173 | 'field ' . $assoc->targetEntity . '#' . $assoc->inversedBy . ' which does not exist.'; | ||
| 174 | } elseif ($targetMetadata->associationMappings[$assoc->inversedBy]->isOwningSide()) { | ||
| 175 | $ce[] = 'The field ' . $class->name . '#' . $fieldName . ' is on the owning side of a ' . | ||
| 176 | 'bi-directional relationship, but the specified inversedBy association on the target-entity ' . | ||
| 177 | $assoc->targetEntity . '#' . $assoc->inversedBy . ' does not contain the required ' . | ||
| 178 | "'mappedBy=\"" . $fieldName . "\"' attribute."; | ||
| 179 | } elseif ($targetMetadata->associationMappings[$assoc->inversedBy]->mappedBy !== $fieldName) { | ||
| 180 | $ce[] = 'The mappings ' . $class->name . '#' . $fieldName . ' and ' . | ||
| 181 | $assoc->targetEntity . '#' . $assoc->inversedBy . ' are ' . | ||
| 182 | 'inconsistent with each other.'; | ||
| 183 | } | ||
| 184 | |||
| 185 | // Verify inverse side/owning side match each other | ||
| 186 | if (array_key_exists($assoc->inversedBy, $targetMetadata->associationMappings)) { | ||
| 187 | $targetAssoc = $targetMetadata->associationMappings[$assoc->inversedBy]; | ||
| 188 | if ($assoc->isOneToOne() && ! $targetAssoc->isOneToOne()) { | ||
| 189 | $ce[] = 'If association ' . $class->name . '#' . $fieldName . ' is one-to-one, then the inversed ' . | ||
| 190 | 'side ' . $targetMetadata->name . '#' . $assoc->inversedBy . ' has to be one-to-one as well.'; | ||
| 191 | } elseif ($assoc->isManyToOne() && ! $targetAssoc->isOneToMany()) { | ||
| 192 | $ce[] = 'If association ' . $class->name . '#' . $fieldName . ' is many-to-one, then the inversed ' . | ||
| 193 | 'side ' . $targetMetadata->name . '#' . $assoc->inversedBy . ' has to be one-to-many.'; | ||
| 194 | } elseif ($assoc->isManyToMany() && ! $targetAssoc->isManyToMany()) { | ||
| 195 | $ce[] = 'If association ' . $class->name . '#' . $fieldName . ' is many-to-many, then the inversed ' . | ||
| 196 | 'side ' . $targetMetadata->name . '#' . $assoc->inversedBy . ' has to be many-to-many as well.'; | ||
| 197 | } | ||
| 198 | } | ||
| 199 | } | ||
| 200 | |||
| 201 | if ($assoc->isOwningSide()) { | ||
| 202 | if ($assoc->isManyToManyOwningSide()) { | ||
| 203 | $identifierColumns = $class->getIdentifierColumnNames(); | ||
| 204 | foreach ($assoc->joinTable->joinColumns as $joinColumn) { | ||
| 205 | if (! in_array($joinColumn->referencedColumnName, $identifierColumns, true)) { | ||
| 206 | $ce[] = "The referenced column name '" . $joinColumn->referencedColumnName . "' " . | ||
| 207 | "has to be a primary key column on the target entity class '" . $class->name . "'."; | ||
| 208 | break; | ||
| 209 | } | ||
| 210 | } | ||
| 211 | |||
| 212 | $identifierColumns = $targetMetadata->getIdentifierColumnNames(); | ||
| 213 | foreach ($assoc->joinTable->inverseJoinColumns as $inverseJoinColumn) { | ||
| 214 | if (! in_array($inverseJoinColumn->referencedColumnName, $identifierColumns, true)) { | ||
| 215 | $ce[] = "The referenced column name '" . $inverseJoinColumn->referencedColumnName . "' " . | ||
| 216 | "has to be a primary key column on the target entity class '" . $targetMetadata->name . "'."; | ||
| 217 | break; | ||
| 218 | } | ||
| 219 | } | ||
| 220 | |||
| 221 | if (count($targetMetadata->getIdentifierColumnNames()) !== count($assoc->joinTable->inverseJoinColumns)) { | ||
| 222 | $ce[] = "The inverse join columns of the many-to-many table '" . $assoc->joinTable->name . "' " . | ||
| 223 | "have to contain to ALL identifier columns of the target entity '" . $targetMetadata->name . "', " . | ||
| 224 | "however '" . implode(', ', array_diff($targetMetadata->getIdentifierColumnNames(), array_values($assoc->relationToTargetKeyColumns))) . | ||
| 225 | "' are missing."; | ||
| 226 | } | ||
| 227 | |||
| 228 | if (count($class->getIdentifierColumnNames()) !== count($assoc->joinTable->joinColumns)) { | ||
| 229 | $ce[] = "The join columns of the many-to-many table '" . $assoc->joinTable->name . "' " . | ||
| 230 | "have to contain to ALL identifier columns of the source entity '" . $class->name . "', " . | ||
| 231 | "however '" . implode(', ', array_diff($class->getIdentifierColumnNames(), array_values($assoc->relationToSourceKeyColumns))) . | ||
| 232 | "' are missing."; | ||
| 233 | } | ||
| 234 | } elseif ($assoc->isToOneOwningSide()) { | ||
| 235 | $identifierColumns = $targetMetadata->getIdentifierColumnNames(); | ||
| 236 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 237 | if (! in_array($joinColumn->referencedColumnName, $identifierColumns, true)) { | ||
| 238 | $ce[] = "The referenced column name '" . $joinColumn->referencedColumnName . "' " . | ||
| 239 | "has to be a primary key column on the target entity class '" . $targetMetadata->name . "'."; | ||
| 240 | } | ||
| 241 | } | ||
| 242 | |||
| 243 | if (count($identifierColumns) !== count($assoc->joinColumns)) { | ||
| 244 | $ids = []; | ||
| 245 | |||
| 246 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 247 | $ids[] = $joinColumn->name; | ||
| 248 | } | ||
| 249 | |||
| 250 | $ce[] = "The join columns of the association '" . $assoc->fieldName . "' " . | ||
| 251 | "have to match to ALL identifier columns of the target entity '" . $targetMetadata->name . "', " . | ||
| 252 | "however '" . implode(', ', array_diff($targetMetadata->getIdentifierColumnNames(), $ids)) . | ||
| 253 | "' are missing."; | ||
| 254 | } | ||
| 255 | } | ||
| 256 | } | ||
| 257 | |||
| 258 | if ($assoc->isOrdered()) { | ||
| 259 | foreach ($assoc->orderBy() as $orderField => $orientation) { | ||
| 260 | if (! $targetMetadata->hasField($orderField) && ! $targetMetadata->hasAssociation($orderField)) { | ||
| 261 | $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' is ordered by a foreign field ' . | ||
| 262 | $orderField . ' that is not a field on the target entity ' . $targetMetadata->name . '.'; | ||
| 263 | continue; | ||
| 264 | } | ||
| 265 | |||
| 266 | if ($targetMetadata->isCollectionValuedAssociation($orderField)) { | ||
| 267 | $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' is ordered by a field ' . | ||
| 268 | $orderField . ' on ' . $targetMetadata->name . ' that is a collection-valued association.'; | ||
| 269 | continue; | ||
| 270 | } | ||
| 271 | |||
| 272 | if ($targetMetadata->isAssociationInverseSide($orderField)) { | ||
| 273 | $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' is ordered by a field ' . | ||
| 274 | $orderField . ' on ' . $targetMetadata->name . ' that is the inverse side of an association.'; | ||
| 275 | continue; | ||
| 276 | } | ||
| 277 | } | ||
| 278 | } | ||
| 279 | } | ||
| 280 | |||
| 281 | if ( | ||
| 282 | ! $class->isInheritanceTypeNone() | ||
| 283 | && ! $class->isRootEntity() | ||
| 284 | && ($class->reflClass !== null && ! $class->reflClass->isAbstract()) | ||
| 285 | && ! $class->isMappedSuperclass | ||
| 286 | && array_search($class->name, $class->discriminatorMap, true) === false | ||
| 287 | ) { | ||
| 288 | $ce[] = "Entity class '" . $class->name . "' is part of inheritance hierarchy, but is " . | ||
| 289 | "not mapped in the root entity '" . $class->rootEntityName . "' discriminator map. " . | ||
| 290 | 'All subclasses must be listed in the discriminator map.'; | ||
| 291 | } | ||
| 292 | |||
| 293 | foreach ($class->subClasses as $subClass) { | ||
| 294 | if (! in_array($class->name, class_parents($subClass), true)) { | ||
| 295 | $ce[] = "According to the discriminator map class '" . $subClass . "' has to be a child " . | ||
| 296 | "of '" . $class->name . "' but these entities are not related through inheritance."; | ||
| 297 | } | ||
| 298 | } | ||
| 299 | |||
| 300 | return $ce; | ||
| 301 | } | ||
| 302 | |||
| 303 | /** | ||
| 304 | * Checks if the Database Schema is in sync with the current metadata state. | ||
| 305 | */ | ||
| 306 | public function schemaInSyncWithMetadata(): bool | ||
| 307 | { | ||
| 308 | return count($this->getUpdateSchemaList()) === 0; | ||
| 309 | } | ||
| 310 | |||
| 311 | /** | ||
| 312 | * Returns the list of missing Database Schema updates. | ||
| 313 | * | ||
| 314 | * @return array<string> | ||
| 315 | */ | ||
| 316 | public function getUpdateSchemaList(): array | ||
| 317 | { | ||
| 318 | $schemaTool = new SchemaTool($this->em); | ||
| 319 | |||
| 320 | $allMetadata = $this->em->getMetadataFactory()->getAllMetadata(); | ||
| 321 | |||
| 322 | return $schemaTool->getUpdateSchemaSql($allMetadata); | ||
| 323 | } | ||
| 324 | |||
| 325 | /** @return list<string> containing the found issues */ | ||
| 326 | private function validatePropertiesTypes(ClassMetadata $class): array | ||
| 327 | { | ||
| 328 | return array_values( | ||
| 329 | array_filter( | ||
| 330 | array_map( | ||
| 331 | function (FieldMapping $fieldMapping) use ($class): string|null { | ||
| 332 | $fieldName = $fieldMapping->fieldName; | ||
| 333 | assert(isset($class->reflFields[$fieldName])); | ||
| 334 | $propertyType = $class->reflFields[$fieldName]->getType(); | ||
| 335 | |||
| 336 | // If the field type is not a built-in type, we cannot check it | ||
| 337 | if (! Type::hasType($fieldMapping->type)) { | ||
| 338 | return null; | ||
| 339 | } | ||
| 340 | |||
| 341 | // If the property type is not a named type, we cannot check it | ||
| 342 | if (! ($propertyType instanceof ReflectionNamedType) || $propertyType->getName() === 'mixed') { | ||
| 343 | return null; | ||
| 344 | } | ||
| 345 | |||
| 346 | $metadataFieldType = $this->findBuiltInType(Type::getType($fieldMapping->type)); | ||
| 347 | |||
| 348 | //If the metadata field type is not a mapped built-in type, we cannot check it | ||
| 349 | if ($metadataFieldType === null) { | ||
| 350 | return null; | ||
| 351 | } | ||
| 352 | |||
| 353 | $propertyType = $propertyType->getName(); | ||
| 354 | |||
| 355 | // If the property type is the same as the metadata field type, we are ok | ||
| 356 | if (in_array($propertyType, $metadataFieldType, true)) { | ||
| 357 | return null; | ||
| 358 | } | ||
| 359 | |||
| 360 | if (is_a($propertyType, BackedEnum::class, true)) { | ||
| 361 | $backingType = (string) (new ReflectionEnum($propertyType))->getBackingType(); | ||
| 362 | |||
| 363 | if (! in_array($backingType, $metadataFieldType, true)) { | ||
| 364 | return sprintf( | ||
| 365 | "The field '%s#%s' has the property type '%s' with a backing type of '%s' that differs from the metadata field type '%s'.", | ||
| 366 | $class->name, | ||
| 367 | $fieldName, | ||
| 368 | $propertyType, | ||
| 369 | $backingType, | ||
| 370 | implode('|', $metadataFieldType), | ||
| 371 | ); | ||
| 372 | } | ||
| 373 | |||
| 374 | if (! isset($fieldMapping->enumType) || $propertyType === $fieldMapping->enumType) { | ||
| 375 | return null; | ||
| 376 | } | ||
| 377 | |||
| 378 | return sprintf( | ||
| 379 | "The field '%s#%s' has the property type '%s' that differs from the metadata enumType '%s'.", | ||
| 380 | $class->name, | ||
| 381 | $fieldName, | ||
| 382 | $propertyType, | ||
| 383 | $fieldMapping->enumType, | ||
| 384 | ); | ||
| 385 | } | ||
| 386 | |||
| 387 | if ( | ||
| 388 | isset($fieldMapping->enumType) | ||
| 389 | && $propertyType !== $fieldMapping->enumType | ||
| 390 | && interface_exists($propertyType) | ||
| 391 | && is_a($fieldMapping->enumType, $propertyType, true) | ||
| 392 | ) { | ||
| 393 | $backingType = (string) (new ReflectionEnum($fieldMapping->enumType))->getBackingType(); | ||
| 394 | |||
| 395 | if (in_array($backingType, $metadataFieldType, true)) { | ||
| 396 | return null; | ||
| 397 | } | ||
| 398 | |||
| 399 | return sprintf( | ||
| 400 | "The field '%s#%s' has the metadata enumType '%s' with a backing type of '%s' that differs from the metadata field type '%s'.", | ||
| 401 | $class->name, | ||
| 402 | $fieldName, | ||
| 403 | $fieldMapping->enumType, | ||
| 404 | $backingType, | ||
| 405 | implode('|', $metadataFieldType), | ||
| 406 | ); | ||
| 407 | } | ||
| 408 | |||
| 409 | if ( | ||
| 410 | $fieldMapping->type === 'json' | ||
| 411 | && in_array($propertyType, ['string', 'int', 'float', 'bool', 'true', 'false', 'null'], true) | ||
| 412 | ) { | ||
| 413 | return null; | ||
| 414 | } | ||
| 415 | |||
| 416 | return sprintf( | ||
| 417 | "The field '%s#%s' has the property type '%s' that differs from the metadata field type '%s' returned by the '%s' DBAL type.", | ||
| 418 | $class->name, | ||
| 419 | $fieldName, | ||
| 420 | $propertyType, | ||
| 421 | implode('|', $metadataFieldType), | ||
| 422 | $fieldMapping->type, | ||
| 423 | ); | ||
| 424 | }, | ||
| 425 | $class->fieldMappings, | ||
| 426 | ), | ||
| 427 | ), | ||
| 428 | ); | ||
| 429 | } | ||
| 430 | |||
| 431 | /** | ||
| 432 | * The exact DBAL type must be used (no subclasses), since consumers of doctrine/orm may have their own | ||
| 433 | * customization around field types. | ||
| 434 | * | ||
| 435 | * @return list<string>|null | ||
| 436 | */ | ||
| 437 | private function findBuiltInType(Type $type): array|null | ||
| 438 | { | ||
| 439 | $typeName = $type::class; | ||
| 440 | |||
| 441 | return self::BUILTIN_TYPES_MAP[$typeName] ?? null; | ||
| 442 | } | ||
| 443 | } | ||
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 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools; | ||
| 6 | |||
| 7 | class ToolEvents | ||
| 8 | { | ||
| 9 | /** | ||
| 10 | * The postGenerateSchemaTable event occurs in SchemaTool#getSchemaFromMetadata() | ||
| 11 | * whenever an entity class is transformed into its table representation. It receives | ||
| 12 | * the current non-complete Schema instance, the Entity Metadata Class instance and | ||
| 13 | * the Schema Table instance of this entity. | ||
| 14 | */ | ||
| 15 | public const postGenerateSchemaTable = 'postGenerateSchemaTable'; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * The postGenerateSchema event is triggered in SchemaTool#getSchemaFromMetadata() | ||
| 19 | * after all entity classes have been transformed into the related Schema structure. | ||
| 20 | * The EventArgs contain the EntityManager and the created Schema instance. | ||
| 21 | */ | ||
| 22 | public const postGenerateSchema = 'postGenerateSchema'; | ||
| 23 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/ToolsException.php b/vendor/doctrine/orm/src/Tools/ToolsException.php new file mode 100644 index 0000000..e5cb973 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/ToolsException.php | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\ORMException; | ||
| 8 | use RuntimeException; | ||
| 9 | use Throwable; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * Tools related Exceptions. | ||
| 13 | */ | ||
| 14 | class ToolsException extends RuntimeException implements ORMException | ||
| 15 | { | ||
| 16 | public static function schemaToolFailure(string $sql, Throwable $e): self | ||
| 17 | { | ||
| 18 | return new self( | ||
| 19 | "Schema-Tool failed with Error '" . $e->getMessage() . "' while executing DDL: " . $sql, | ||
| 20 | 0, | ||
| 21 | $e, | ||
| 22 | ); | ||
| 23 | } | ||
| 24 | } | ||
