summaryrefslogtreecommitdiff
path: root/vendor/doctrine/orm/src/Tools
diff options
context:
space:
mode:
authorpolo <ordipolo@gmx.fr>2024-08-13 23:45:21 +0200
committerpolo <ordipolo@gmx.fr>2024-08-13 23:45:21 +0200
commitbf6655a534a6775d30cafa67bd801276bda1d98d (patch)
treec6381e3f6c81c33eab72508f410b165ba05f7e9c /vendor/doctrine/orm/src/Tools
parent94d67a4b51f8e62e7d518cce26a526ae1ec48278 (diff)
downloadAppliGestionPHP-bf6655a534a6775d30cafa67bd801276bda1d98d.zip
VERSION 0.2 doctrine ORM et entités
Diffstat (limited to 'vendor/doctrine/orm/src/Tools')
-rw-r--r--vendor/doctrine/orm/src/Tools/AttachEntityListenersListener.php69
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/AbstractEntityManagerCommand.php25
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/CollectionRegionCommand.php119
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/EntityRegionCommand.php110
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/MetadataCommand.php52
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryCommand.php54
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryRegionCommand.php101
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/ResultCommand.php65
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/GenerateProxiesCommand.php96
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/InfoCommand.php80
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/MappingDescribeCommand.php279
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/RunDqlCommand.php118
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/AbstractCommand.php39
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/CreateCommand.php75
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/DropCommand.php116
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/UpdateCommand.php147
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/ValidateSchemaCommand.php89
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/ConsoleRunner.php88
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider.php14
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/ConnectionFromManagerProvider.php26
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/SingleManagerProvider.php31
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/UnknownManagerException.php23
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/MetadataFilter.php92
-rw-r--r--vendor/doctrine/orm/src/Tools/Debug.php158
-rw-r--r--vendor/doctrine/orm/src/Tools/DebugUnitOfWorkListener.php144
-rw-r--r--vendor/doctrine/orm/src/Tools/Event/GenerateSchemaEventArgs.php33
-rw-r--r--vendor/doctrine/orm/src/Tools/Event/GenerateSchemaTableEventArgs.php40
-rw-r--r--vendor/doctrine/orm/src/Tools/Exception/MissingColumnException.php23
-rw-r--r--vendor/doctrine/orm/src/Tools/Exception/NotSupported.php16
-rw-r--r--vendor/doctrine/orm/src/Tools/Pagination/CountOutputWalker.php125
-rw-r--r--vendor/doctrine/orm/src/Tools/Pagination/CountWalker.php68
-rw-r--r--vendor/doctrine/orm/src/Tools/Pagination/Exception/RowNumberOverFunctionNotEnabled.php16
-rw-r--r--vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryOutputWalker.php544
-rw-r--r--vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryWalker.php155
-rw-r--r--vendor/doctrine/orm/src/Tools/Pagination/Paginator.php263
-rw-r--r--vendor/doctrine/orm/src/Tools/Pagination/RootTypeWalker.php48
-rw-r--r--vendor/doctrine/orm/src/Tools/Pagination/RowNumberOverFunction.php40
-rw-r--r--vendor/doctrine/orm/src/Tools/Pagination/WhereInWalker.php116
-rw-r--r--vendor/doctrine/orm/src/Tools/ResolveTargetEntityListener.php117
-rw-r--r--vendor/doctrine/orm/src/Tools/SchemaTool.php932
-rw-r--r--vendor/doctrine/orm/src/Tools/SchemaValidator.php443
-rw-r--r--vendor/doctrine/orm/src/Tools/ToolEvents.php23
-rw-r--r--vendor/doctrine/orm/src/Tools/ToolsException.php24
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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools;
6
7use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
8use Doctrine\ORM\Events;
9use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
10
11use function assert;
12use function ltrim;
13
14/**
15 * Mechanism to programmatically attach entity listeners.
16 */
17class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\ORM\Tools\Console\EntityManagerProvider;
9use Symfony\Component\Console\Command\Command;
10use Symfony\Component\Console\Input\InputInterface;
11
12abstract 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
6
7use Doctrine\ORM\Cache;
8use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
9use InvalidArgumentException;
10use Symfony\Component\Console\Input\InputArgument;
11use Symfony\Component\Console\Input\InputInterface;
12use Symfony\Component\Console\Input\InputOption;
13use Symfony\Component\Console\Output\OutputInterface;
14use Symfony\Component\Console\Style\SymfonyStyle;
15
16use function sprintf;
17
18/**
19 * Command to clear a collection cache region.
20 */
21class 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'
34The <info>%command.name%</info> command is meant to clear a second-level cache collection regions for an associated Entity Manager.
35It is possible to delete/invalidate all collection region, a specific collection region or flushes the cache provider.
36
37The execution type differ on how you execute the command.
38If 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
42To invalidate a specific entry you should use :
43
44<info>%command.name% 'Entities\MyEntity' 'collectionName' 1</info>
45
46If you want to invalidate all entries for the all collection regions:
47
48<info>%command.name% --all</info>
49
50Alternatively, 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
54Finally, be aware that if <info>--flush</info> option is passed,
55not all cache providers are able to flush entries, because of a limitation of its execution nature.
56EOT);
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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
6
7use Doctrine\ORM\Cache;
8use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
9use InvalidArgumentException;
10use Symfony\Component\Console\Input\InputArgument;
11use Symfony\Component\Console\Input\InputInterface;
12use Symfony\Component\Console\Input\InputOption;
13use Symfony\Component\Console\Output\OutputInterface;
14use Symfony\Component\Console\Style\SymfonyStyle;
15
16use function sprintf;
17
18/**
19 * Command to clear a entity cache region.
20 */
21class 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'
33The <info>%command.name%</info> command is meant to clear a second-level cache entity region for an associated Entity Manager.
34It is possible to delete/invalidate all entity region, a specific entity region or flushes the cache provider.
35
36The execution type differ on how you execute the command.
37If 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
41To invalidate a specific entry you should use :
42
43<info>%command.name% 'Entities\MyEntity' 1</info>
44
45If you want to invalidate all entries for the all entity regions:
46
47<info>%command.name% --all</info>
48
49Alternatively, 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
53Finally, be aware that if <info>--flush</info> option is passed,
54not all cache providers are able to flush entries, because of a limitation of its execution nature.
55EOT);
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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
6
7use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
8use InvalidArgumentException;
9use Symfony\Component\Console\Input\InputInterface;
10use Symfony\Component\Console\Input\InputOption;
11use Symfony\Component\Console\Output\OutputInterface;
12use 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 */
19class 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'
28The <info>%command.name%</info> command is meant to clear the metadata cache of associated Entity Manager.
29EOT);
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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
6
7use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
8use InvalidArgumentException;
9use LogicException;
10use Symfony\Component\Cache\Adapter\ApcuAdapter;
11use Symfony\Component\Console\Input\InputInterface;
12use Symfony\Component\Console\Input\InputOption;
13use Symfony\Component\Console\Output\OutputInterface;
14use 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 */
21class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
6
7use Doctrine\ORM\Cache;
8use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
9use InvalidArgumentException;
10use Symfony\Component\Console\Input\InputArgument;
11use Symfony\Component\Console\Input\InputInterface;
12use Symfony\Component\Console\Input\InputOption;
13use Symfony\Component\Console\Output\OutputInterface;
14use Symfony\Component\Console\Style\SymfonyStyle;
15
16use function sprintf;
17
18/**
19 * Command to clear a query cache region.
20 */
21class 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'
32The <info>%command.name%</info> command is meant to clear a second-level cache query region for an associated Entity Manager.
33It is possible to delete/invalidate all query region, a specific query region or flushes the cache provider.
34
35The execution type differ on how you execute the command.
36If you want to invalidate all entries for the default query region this command would do the work:
37
38<info>%command.name%</info>
39
40To invalidate entries for a specific query region you should use :
41
42<info>%command.name% my_region_name</info>
43
44If you want to invalidate all entries for the all query region:
45
46<info>%command.name% --all</info>
47
48Alternatively, if you want to flush the configured cache provider use this command:
49
50<info>%command.name% my_region_name --flush</info>
51
52Finally, be aware that if <info>--flush</info> option is passed,
53not all cache providers are able to flush entries, because of a limitation of its execution nature.
54EOT);
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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
6
7use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
8use InvalidArgumentException;
9use Symfony\Component\Console\Input\InputInterface;
10use Symfony\Component\Console\Input\InputOption;
11use Symfony\Component\Console\Output\OutputInterface;
12use 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 */
19class 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'
28The <info>%command.name%</info> command is meant to clear the result cache of associated Entity Manager.
29It is possible to invalidate all cache entries at once - called delete -, or flushes the cache provider
30instance completely.
31
32The execution type differ on how you execute the command.
33If 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
37Alternatively, if you want to flush the cache provider using this command:
38
39<info>%command.name% --flush</info>
40
41Finally, be aware that if <info>--flush</info> option is passed, not all cache providers are able to flush entries,
42because of a limitation of its execution nature.
43EOT);
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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command;
6
7use Doctrine\ORM\Tools\Console\MetadataFilter;
8use InvalidArgumentException;
9use Symfony\Component\Console\Input\InputArgument;
10use Symfony\Component\Console\Input\InputInterface;
11use Symfony\Component\Console\Input\InputOption;
12use Symfony\Component\Console\Output\OutputInterface;
13use Symfony\Component\Console\Style\SymfonyStyle;
14
15use function file_exists;
16use function is_dir;
17use function is_writable;
18use function mkdir;
19use function realpath;
20use function sprintf;
21
22/**
23 * Command to (re)generate the proxy classes used by doctrine.
24 *
25 * @link www.doctrine-project.org
26 */
27class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command;
6
7use Doctrine\ORM\Mapping\MappingException;
8use Symfony\Component\Console\Input\InputInterface;
9use Symfony\Component\Console\Input\InputOption;
10use Symfony\Component\Console\Output\OutputInterface;
11use Symfony\Component\Console\Style\SymfonyStyle;
12
13use function count;
14use function sprintf;
15
16/**
17 * Show information about mapped entities.
18 *
19 * @link www.doctrine-project.org
20 */
21class 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'
29The <info>%command.name%</info> shows basic information about which
30entities exist and possibly if their mapping information contains errors or
31not.
32EOT);
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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\ORM\Mapping\AssociationMapping;
9use Doctrine\ORM\Mapping\ClassMetadata;
10use Doctrine\ORM\Mapping\FieldMapping;
11use Doctrine\Persistence\Mapping\MappingException;
12use InvalidArgumentException;
13use Symfony\Component\Console\Input\InputArgument;
14use Symfony\Component\Console\Input\InputInterface;
15use Symfony\Component\Console\Input\InputOption;
16use Symfony\Component\Console\Output\OutputInterface;
17use Symfony\Component\Console\Style\SymfonyStyle;
18
19use function array_filter;
20use function array_map;
21use function array_merge;
22use function count;
23use function current;
24use function get_debug_type;
25use function implode;
26use function is_array;
27use function is_bool;
28use function is_object;
29use function is_scalar;
30use function json_encode;
31use function preg_match;
32use function preg_quote;
33use function print_r;
34use function sprintf;
35
36use const JSON_PRETTY_PRINT;
37use const JSON_THROW_ON_ERROR;
38use const JSON_UNESCAPED_SLASHES;
39use const JSON_UNESCAPED_UNICODE;
40
41/**
42 * Show information about mapped entities.
43 *
44 * @link www.doctrine-project.org
45 */
46final 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'
55The %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
59Or:
60
61 <info>%command.full_name%</info> MyEntity
62EOT);
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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command;
6
7use Doctrine\ORM\Tools\Debug;
8use LogicException;
9use RuntimeException;
10use Symfony\Component\Console\Input\InputArgument;
11use Symfony\Component\Console\Input\InputInterface;
12use Symfony\Component\Console\Input\InputOption;
13use Symfony\Component\Console\Output\OutputInterface;
14use Symfony\Component\Console\Style\SymfonyStyle;
15
16use function constant;
17use function defined;
18use function is_numeric;
19use function sprintf;
20use function str_replace;
21use function strtoupper;
22
23/**
24 * Command to execute DQL queries in a given EntityManager.
25 *
26 * @link www.doctrine-project.org
27 */
28class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\SchemaTool;
6
7use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
8use Doctrine\ORM\Tools\SchemaTool;
9use Symfony\Component\Console\Input\InputInterface;
10use Symfony\Component\Console\Output\OutputInterface;
11use Symfony\Component\Console\Style\SymfonyStyle;
12
13/**
14 * Base class for CreateCommand, DropCommand and UpdateCommand.
15 *
16 * @link www.doctrine-project.org
17 */
18abstract 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\SchemaTool;
6
7use Doctrine\ORM\Tools\SchemaTool;
8use Symfony\Component\Console\Input\InputInterface;
9use Symfony\Component\Console\Input\InputOption;
10use Symfony\Component\Console\Output\OutputInterface;
11use Symfony\Component\Console\Style\SymfonyStyle;
12
13use 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 */
20class 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'
29Processes 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
32by the ORM, you can use a DBAL functionality to filter the tables and sequences down
33on 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 });
42EOT);
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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\SchemaTool;
6
7use Doctrine\ORM\Tools\SchemaTool;
8use Symfony\Component\Console\Input\InputInterface;
9use Symfony\Component\Console\Input\InputOption;
10use Symfony\Component\Console\Output\OutputInterface;
11use Symfony\Component\Console\Style\SymfonyStyle;
12
13use function count;
14use 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 */
21class 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'
32Processes the schema and either drop the database schema of EntityManager Storage Connection or generate the SQL output.
33Beware 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
36by the ORM, you can use a DBAL functionality to filter the tables and sequences down
37on 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 });
46EOT);
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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\SchemaTool;
6
7use Doctrine\Deprecations\Deprecation;
8use Doctrine\ORM\Tools\SchemaTool;
9use Symfony\Component\Console\Input\InputInterface;
10use Symfony\Component\Console\Input\InputOption;
11use Symfony\Component\Console\Output\OutputInterface;
12use Symfony\Component\Console\Style\SymfonyStyle;
13
14use function count;
15use 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 */
23class 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'
36The <info>%command.name%</info> command generates the SQL needed to
37synchronize the database schema with the current mapping metadata of the
38default entity manager.
39
40For example, if you add metadata for a new column to an entity, this command
41would generate and output the SQL needed to add the new column to the database:
42
43<info>%command.name% --dump-sql</info>
44
45Alternatively, you can execute the generated queries:
46
47<info>%command.name% --force</info>
48
49If both options are specified, the queries are output and then executed:
50
51<info>%command.name% --dump-sql --force</info>
52
53Finally, be aware that this task will drop all database assets (e.g. tables,
54etc) that are *not* described by the current metadata. In other words, without
55this option, this task leaves untouched any "extra" tables that exist in the
56database, 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
59by the ORM, you can use a DBAL functionality to filter the tables and sequences down
60on 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 });
69EOT);
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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command;
6
7use Doctrine\ORM\Tools\SchemaValidator;
8use Symfony\Component\Console\Input\InputInterface;
9use Symfony\Component\Console\Input\InputOption;
10use Symfony\Component\Console\Output\OutputInterface;
11use Symfony\Component\Console\Style\SymfonyStyle;
12
13use function count;
14use function sprintf;
15
16/**
17 * Command to validate that the current mapping is valid.
18 *
19 * @link www.doctrine-project.com
20 */
21class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console;
6
7use Composer\InstalledVersions;
8use Doctrine\DBAL\Tools\Console as DBALConsole;
9use Doctrine\ORM\Tools\Console\EntityManagerProvider\ConnectionFromManagerProvider;
10use OutOfBoundsException;
11use Symfony\Component\Console\Application;
12use Symfony\Component\Console\Command\Command as SymfonyCommand;
13
14use function assert;
15use function class_exists;
16
17/**
18 * Handles running the Console Tools inside Symfony Console context.
19 */
20final 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console;
6
7use Doctrine\ORM\EntityManagerInterface;
8
9interface 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\EntityManagerProvider;
6
7use Doctrine\DBAL\Connection;
8use Doctrine\DBAL\Tools\Console\ConnectionProvider;
9use Doctrine\ORM\Tools\Console\EntityManagerProvider;
10
11final 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\EntityManagerProvider;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\ORM\Tools\Console\EntityManagerProvider;
9
10final 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\EntityManagerProvider;
6
7use OutOfBoundsException;
8
9use function implode;
10use function sprintf;
11
12final 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console;
6
7use ArrayIterator;
8use Countable;
9use Doctrine\Persistence\Mapping\ClassMetadata;
10use FilterIterator;
11use RuntimeException;
12
13use function assert;
14use function count;
15use function iterator_to_array;
16use function preg_match;
17use 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 */
24class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools;
6
7use ArrayIterator;
8use ArrayObject;
9use DateTimeInterface;
10use Doctrine\Common\Collections\Collection;
11use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
12use Doctrine\Persistence\Proxy;
13use stdClass;
14
15use function array_keys;
16use function count;
17use function end;
18use function explode;
19use function extension_loaded;
20use function html_entity_decode;
21use function ini_get;
22use function ini_set;
23use function is_array;
24use function is_object;
25use function ob_end_clean;
26use function ob_get_contents;
27use function ob_start;
28use function strip_tags;
29use function var_dump;
30
31/**
32 * Static class containing most used debug methods.
33 *
34 * @internal
35 *
36 * @link www.doctrine-project.org
37 */
38final 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\ORM\Event\OnFlushEventArgs;
9use Doctrine\ORM\PersistentCollection;
10use Doctrine\ORM\UnitOfWork;
11use ReflectionObject;
12
13use function count;
14use function fclose;
15use function fopen;
16use function fwrite;
17use function gettype;
18use function is_object;
19use 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 */
25class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Event;
6
7use Doctrine\Common\EventArgs;
8use Doctrine\DBAL\Schema\Schema;
9use Doctrine\ORM\EntityManagerInterface;
10
11/**
12 * Event Args used for the Events::postGenerateSchema event.
13 *
14 * @link www.doctrine-project.com
15 */
16class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Event;
6
7use Doctrine\Common\EventArgs;
8use Doctrine\DBAL\Schema\Schema;
9use Doctrine\DBAL\Schema\Table;
10use Doctrine\ORM\Mapping\ClassMetadata;
11
12/**
13 * Event Args used for the Events::postGenerateSchemaTable event.
14 *
15 * @link www.doctrine-project.com
16 */
17class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Exception;
6
7use Doctrine\ORM\Exception\ORMException;
8use LogicException;
9
10use function sprintf;
11
12final 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Exception;
6
7use Doctrine\ORM\Exception\SchemaToolException;
8use LogicException;
9
10final 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Pagination;
6
7use Doctrine\DBAL\Platforms\AbstractPlatform;
8use Doctrine\DBAL\Platforms\SQLServerPlatform;
9use Doctrine\ORM\Query;
10use Doctrine\ORM\Query\AST\SelectStatement;
11use Doctrine\ORM\Query\Parser;
12use Doctrine\ORM\Query\ParserResult;
13use Doctrine\ORM\Query\ResultSetMapping;
14use Doctrine\ORM\Query\SqlWalker;
15use RuntimeException;
16
17use function array_diff;
18use function array_keys;
19use function assert;
20use function count;
21use function implode;
22use function reset;
23use 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 */
40class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Pagination;
6
7use Doctrine\ORM\Query\AST\AggregateExpression;
8use Doctrine\ORM\Query\AST\PathExpression;
9use Doctrine\ORM\Query\AST\SelectExpression;
10use Doctrine\ORM\Query\AST\SelectStatement;
11use Doctrine\ORM\Query\TreeWalkerAdapter;
12use RuntimeException;
13
14use function count;
15use function reset;
16
17/**
18 * Replaces the selectClause of the AST with a COUNT statement.
19 */
20class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Pagination\Exception;
6
7use Doctrine\ORM\Exception\ORMException;
8use LogicException;
9
10final 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Pagination;
6
7use Doctrine\DBAL\Platforms\AbstractPlatform;
8use Doctrine\DBAL\Platforms\DB2Platform;
9use Doctrine\DBAL\Platforms\OraclePlatform;
10use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
11use Doctrine\DBAL\Platforms\SQLServerPlatform;
12use Doctrine\ORM\EntityManagerInterface;
13use Doctrine\ORM\Mapping\QuoteStrategy;
14use Doctrine\ORM\OptimisticLockException;
15use Doctrine\ORM\Query;
16use Doctrine\ORM\Query\AST\OrderByClause;
17use Doctrine\ORM\Query\AST\PathExpression;
18use Doctrine\ORM\Query\AST\SelectExpression;
19use Doctrine\ORM\Query\AST\SelectStatement;
20use Doctrine\ORM\Query\AST\Subselect;
21use Doctrine\ORM\Query\Parser;
22use Doctrine\ORM\Query\ParserResult;
23use Doctrine\ORM\Query\QueryException;
24use Doctrine\ORM\Query\ResultSetMapping;
25use Doctrine\ORM\Query\SqlWalker;
26use RuntimeException;
27
28use function array_diff;
29use function array_keys;
30use function assert;
31use function count;
32use function implode;
33use function in_array;
34use function is_string;
35use function method_exists;
36use function preg_replace;
37use function reset;
38use function sprintf;
39use function strrpos;
40use 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 */
53class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Pagination;
6
7use Doctrine\DBAL\Types\Type;
8use Doctrine\ORM\Query;
9use Doctrine\ORM\Query\AST\Functions\IdentityFunction;
10use Doctrine\ORM\Query\AST\Node;
11use Doctrine\ORM\Query\AST\PathExpression;
12use Doctrine\ORM\Query\AST\SelectExpression;
13use Doctrine\ORM\Query\AST\SelectStatement;
14use Doctrine\ORM\Query\TreeWalkerAdapter;
15use RuntimeException;
16
17use function count;
18use function is_string;
19use function reset;
20
21/**
22 * Replaces the selectClause of the AST with a SELECT DISTINCT root.id equivalent.
23 */
24class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Pagination;
6
7use ArrayIterator;
8use Countable;
9use Doctrine\Common\Collections\Collection;
10use Doctrine\ORM\Internal\SQLResultCasing;
11use Doctrine\ORM\NoResultException;
12use Doctrine\ORM\Query;
13use Doctrine\ORM\Query\Parameter;
14use Doctrine\ORM\Query\Parser;
15use Doctrine\ORM\Query\ResultSetMapping;
16use Doctrine\ORM\QueryBuilder;
17use IteratorAggregate;
18use Traversable;
19
20use function array_key_exists;
21use function array_map;
22use function array_sum;
23use function assert;
24use 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 */
32class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Pagination;
6
7use Doctrine\ORM\Query\AST;
8use Doctrine\ORM\Query\SqlWalker;
9use Doctrine\ORM\Utility\PersisterHelper;
10use RuntimeException;
11
12use function count;
13use 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 */
25final 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Pagination;
6
7use Doctrine\ORM\Query\AST\Functions\FunctionNode;
8use Doctrine\ORM\Query\AST\OrderByClause;
9use Doctrine\ORM\Query\Parser;
10use Doctrine\ORM\Query\SqlWalker;
11use Doctrine\ORM\Tools\Pagination\Exception\RowNumberOverFunctionNotEnabled;
12
13use function trim;
14
15/**
16 * RowNumberOverFunction
17 *
18 * Provides ROW_NUMBER() OVER(ORDER BY...) construct for use in LimitSubqueryOutputWalker
19 */
20class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Pagination;
6
7use Doctrine\ORM\Query\AST\ArithmeticExpression;
8use Doctrine\ORM\Query\AST\ConditionalExpression;
9use Doctrine\ORM\Query\AST\ConditionalPrimary;
10use Doctrine\ORM\Query\AST\ConditionalTerm;
11use Doctrine\ORM\Query\AST\InListExpression;
12use Doctrine\ORM\Query\AST\InputParameter;
13use Doctrine\ORM\Query\AST\NullComparisonExpression;
14use Doctrine\ORM\Query\AST\PathExpression;
15use Doctrine\ORM\Query\AST\SelectStatement;
16use Doctrine\ORM\Query\AST\SimpleArithmeticExpression;
17use Doctrine\ORM\Query\AST\WhereClause;
18use Doctrine\ORM\Query\TreeWalkerAdapter;
19use RuntimeException;
20
21use function count;
22use 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 */
33class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools;
6
7use Doctrine\Common\EventSubscriber;
8use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
9use Doctrine\ORM\Event\OnClassMetadataNotFoundEventArgs;
10use Doctrine\ORM\Events;
11use Doctrine\ORM\Mapping\AssociationMapping;
12use Doctrine\ORM\Mapping\ClassMetadata;
13
14use function array_key_exists;
15use function array_replace_recursive;
16use function ltrim;
17
18/**
19 * ResolveTargetEntityListener
20 *
21 * Mechanism to overwrite interfaces or classes specified as association
22 * targets.
23 */
24class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools;
6
7use BackedEnum;
8use Doctrine\DBAL\Platforms\AbstractPlatform;
9use Doctrine\DBAL\Schema\AbstractAsset;
10use Doctrine\DBAL\Schema\AbstractSchemaManager;
11use Doctrine\DBAL\Schema\Index;
12use Doctrine\DBAL\Schema\Schema;
13use Doctrine\DBAL\Schema\Table;
14use Doctrine\ORM\EntityManagerInterface;
15use Doctrine\ORM\Mapping\AssociationMapping;
16use Doctrine\ORM\Mapping\ClassMetadata;
17use Doctrine\ORM\Mapping\DiscriminatorColumnMapping;
18use Doctrine\ORM\Mapping\FieldMapping;
19use Doctrine\ORM\Mapping\JoinColumnMapping;
20use Doctrine\ORM\Mapping\ManyToManyOwningSideMapping;
21use Doctrine\ORM\Mapping\MappingException;
22use Doctrine\ORM\Mapping\QuoteStrategy;
23use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
24use Doctrine\ORM\Tools\Event\GenerateSchemaTableEventArgs;
25use Doctrine\ORM\Tools\Exception\MissingColumnException;
26use Doctrine\ORM\Tools\Exception\NotSupported;
27use Throwable;
28
29use function array_diff;
30use function array_diff_key;
31use function array_filter;
32use function array_flip;
33use function array_intersect_key;
34use function assert;
35use function count;
36use function current;
37use function implode;
38use function in_array;
39use function is_numeric;
40use 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 */
48class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools;
6
7use BackedEnum;
8use Doctrine\DBAL\Types\AsciiStringType;
9use Doctrine\DBAL\Types\BigIntType;
10use Doctrine\DBAL\Types\BooleanType;
11use Doctrine\DBAL\Types\DecimalType;
12use Doctrine\DBAL\Types\FloatType;
13use Doctrine\DBAL\Types\GuidType;
14use Doctrine\DBAL\Types\IntegerType;
15use Doctrine\DBAL\Types\JsonType;
16use Doctrine\DBAL\Types\SimpleArrayType;
17use Doctrine\DBAL\Types\SmallIntType;
18use Doctrine\DBAL\Types\StringType;
19use Doctrine\DBAL\Types\TextType;
20use Doctrine\DBAL\Types\Type;
21use Doctrine\ORM\EntityManagerInterface;
22use Doctrine\ORM\Mapping\ClassMetadata;
23use Doctrine\ORM\Mapping\FieldMapping;
24use ReflectionEnum;
25use ReflectionNamedType;
26
27use function array_diff;
28use function array_filter;
29use function array_key_exists;
30use function array_map;
31use function array_push;
32use function array_search;
33use function array_values;
34use function assert;
35use function class_exists;
36use function class_parents;
37use function count;
38use function implode;
39use function in_array;
40use function interface_exists;
41use function is_a;
42use function sprintf;
43
44/**
45 * Performs strict validation of the mapping schema
46 *
47 * @link www.doctrine-project.com
48 */
49class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools;
6
7class 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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools;
6
7use Doctrine\ORM\Exception\ORMException;
8use RuntimeException;
9use Throwable;
10
11/**
12 * Tools related Exceptions.
13 */
14class 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}