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 | } | ||