Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
63.33% |
57 / 90 |
|
28.57% |
2 / 7 |
CRAP | |
0.00% |
0 / 1 |
OrmBuilder | |
63.33% |
57 / 90 |
|
28.57% |
2 / 7 |
90.99 | |
0.00% |
0 / 1 |
__construct | |
20.00% |
2 / 10 |
|
0.00% |
0 / 1 |
24.43 | |||
getGeneratedNamespace | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLogEntity | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
runMigrations | |
40.91% |
9 / 22 |
|
0.00% |
0 / 1 |
17.11 | |||
toDoctrineClass | |
53.85% |
7 / 13 |
|
0.00% |
0 / 1 |
5.57 | |||
isEmptyPath | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
4.07 | |||
createEntityManager | |
85.71% |
30 / 35 |
|
0.00% |
0 / 1 |
10.29 |
1 | <?php |
2 | namespace Apie\DoctrineEntityDatalayer; |
3 | |
4 | use Apie\Core\BoundedContext\BoundedContextId; |
5 | use Apie\Core\Entities\EntityInterface; |
6 | use Apie\DoctrineEntityConverter\OrmBuilder as DoctrineEntityConverterOrmBuilder; |
7 | use Apie\DoctrineEntityDatalayer\Exceptions\CouldNotUpdateDatabaseAutomatically; |
8 | use Apie\DoctrineEntityDatalayer\Middleware\RunMigrationsOnConnect; |
9 | use Apie\StorageMetadata\Interfaces\StorageDtoInterface; |
10 | use Apie\StorageMetadataBuilder\Interfaces\RootObjectInterface; |
11 | use Doctrine\Bundle\DoctrineBundle\Middleware\DebugMiddleware; |
12 | use Doctrine\Common\EventManager; |
13 | use Doctrine\DBAL\DriverManager; |
14 | use Doctrine\DBAL\Exception\DriverException; |
15 | use Doctrine\DBAL\Exception\MalformedDsnException; |
16 | use Doctrine\DBAL\Schema\AbstractAsset; |
17 | use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; |
18 | use Doctrine\DBAL\Tools\DsnParser; |
19 | use Doctrine\ORM\EntityManager; |
20 | use Doctrine\ORM\EntityManagerInterface; |
21 | use Doctrine\ORM\ORMSetup; |
22 | use Doctrine\ORM\Tools\SchemaTool; |
23 | use FilesystemIterator; |
24 | use Psr\Cache\CacheItemPoolInterface; |
25 | use RecursiveDirectoryIterator; |
26 | use ReflectionClass; |
27 | use RuntimeException; |
28 | |
29 | class OrmBuilder |
30 | { |
31 | private ?EntityManagerInterface $createdEntityManager = null; |
32 | |
33 | private bool $isModified = false; |
34 | /** |
35 | * @var array<string, mixed> $connectionConfig |
36 | */ |
37 | private readonly array $connectionConfig; |
38 | /** |
39 | * @param array<string, mixed> $connectionConfig |
40 | */ |
41 | public function __construct( |
42 | private readonly DoctrineEntityConverterOrmBuilder $ormBuilder, |
43 | private bool $buildOnce, |
44 | private bool $runMigrations, |
45 | private readonly bool $devMode, |
46 | private readonly ?string $proxyDir, |
47 | private readonly ?CacheItemPoolInterface $cache, |
48 | private readonly string $path, |
49 | array $connectionConfig, |
50 | private readonly ?DebugMiddleware $debugMiddleware = null |
51 | ) { |
52 | // https://github.com/doctrine/dbal/issues/3209 |
53 | if (isset($connectionConfig['url'])) { |
54 | $parser = new DsnParser(['mysql' => 'pdo_mysql', 'postgres' => 'pdo_pgsql', 'sqlite' => 'pdo_sqlite']); |
55 | /** @var array<string, mixed> $options */ |
56 | $options = []; |
57 | try { |
58 | $options = $parser->parse($connectionConfig['url']); |
59 | } catch (MalformedDsnException) { |
60 | } |
61 | foreach ($options as $option => $value) { |
62 | if (!isset($connectionConfig[$option]) && $value !== null) { |
63 | $connectionConfig[$option] = $value; |
64 | } |
65 | } |
66 | unset($connectionConfig['url']); |
67 | } |
68 | $this->connectionConfig = $connectionConfig; |
69 | } |
70 | public function getGeneratedNamespace(): string |
71 | { |
72 | return 'Generated\\ApieEntities' . $this->ormBuilder->getLastGeneratedCode($this->path)->getId() . '\\'; |
73 | } |
74 | |
75 | public function getLogEntity(): ?EntityInterface |
76 | { |
77 | if ($this->isModified) { |
78 | return $this->ormBuilder->getLastGeneratedCode($this->path); |
79 | } |
80 | return null; |
81 | } |
82 | |
83 | protected function runMigrations(EntityManagerInterface $entityManager, bool $firstCall = true): void |
84 | { |
85 | $tool = new SchemaTool($entityManager); |
86 | $classes = $entityManager->getMetadataFactory()->getAllMetadata(); |
87 | $statementCounts = []; |
88 | try { |
89 | $sql = $tool->getUpdateSchemaSql($classes); |
90 | // for some reason the order is not the order we should execute them..... |
91 | while (!empty($sql)) { |
92 | try { |
93 | do { |
94 | $statement = array_shift($sql); |
95 | $entityManager->getConnection()->executeStatement($statement); |
96 | } while (!empty($sql)); |
97 | } catch (DriverException $driverException) { |
98 | $statementCounts[$statement] ??= 0; |
99 | $statementCounts[$statement]++; |
100 | if ($statementCounts[$statement] > 5) { |
101 | throw $driverException; |
102 | } |
103 | array_push($sql, $statement); |
104 | } |
105 | } |
106 | } catch (DriverException $driverException) { |
107 | if ($firstCall) { |
108 | $sql = $tool->getDropDatabaseSQL(); |
109 | foreach ($sql as $statement) { |
110 | $entityManager->getConnection()->executeStatement($statement); |
111 | } |
112 | $this->runMigrations($entityManager, false); |
113 | } |
114 | throw new CouldNotUpdateDatabaseAutomatically($driverException); |
115 | } |
116 | $this->runMigrations = false; |
117 | } |
118 | |
119 | /** |
120 | * @param ReflectionClass<EntityInterface> $class |
121 | * @return ReflectionClass<StorageDtoInterface> |
122 | */ |
123 | public function toDoctrineClass(ReflectionClass $class, ?BoundedContextId $boundedContextId = null): ReflectionClass |
124 | { |
125 | $manager = $this->createEntityManager(); |
126 | foreach ($manager->getMetadataFactory()->getAllMetadata() as $metadata) { |
127 | $refl = new ReflectionClass($metadata->getName()); |
128 | if (in_array(RootObjectInterface::class, $refl->getInterfaceNames())) { |
129 | $originalClass = $refl->getMethod('getClassReference')->invoke(null); |
130 | if ($originalClass->name === $class->name) { |
131 | return $refl; |
132 | } |
133 | } |
134 | } |
135 | throw new RuntimeException( |
136 | sprintf( |
137 | 'Could not find Doctrine class to handle %s', |
138 | $class->name |
139 | ) |
140 | ); |
141 | } |
142 | |
143 | private function isEmptyPath(): bool |
144 | { |
145 | if (!file_exists($this->path) || !is_dir($this->path)) { |
146 | return true; |
147 | } |
148 | $di = new RecursiveDirectoryIterator($this->path, FilesystemIterator::SKIP_DOTS); |
149 | foreach ($di as $ignored) { |
150 | return false; |
151 | } |
152 | |
153 | return true; |
154 | } |
155 | |
156 | public function createEntityManager(): EntityManagerInterface |
157 | { |
158 | $this->isModified = false; |
159 | if (!$this->buildOnce || $this->isEmptyPath()) { |
160 | $this->isModified = $this->ormBuilder->createOrm($this->path); |
161 | $this->buildOnce = true; |
162 | } |
163 | $path = $this->path . '/build' . $this->ormBuilder->getLastGeneratedCode($this->path)->getId(); |
164 | |
165 | $config = ORMSetup::createAttributeMetadataConfiguration( |
166 | [$path], |
167 | $this->devMode, |
168 | $this->proxyDir, |
169 | $this->devMode ? null : $this->cache |
170 | ); |
171 | $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); |
172 | $config->setLazyGhostObjectEnabled(true); |
173 | $config->setSchemaAssetsFilter(static function (string|AbstractAsset $assetName): bool { |
174 | if ($assetName instanceof AbstractAsset) { |
175 | $assetName = $assetName->getName(); |
176 | } |
177 | |
178 | if ($assetName === 'doctrine_migration_versions') { |
179 | return true; |
180 | } |
181 | |
182 | return (bool) preg_match("~^apie_~i", $assetName); |
183 | }); |
184 | $middlewares = []; |
185 | if ($this->debugMiddleware) { |
186 | $middlewares[] = $this->debugMiddleware; |
187 | } |
188 | if ($this->runMigrations) { |
189 | $middlewares[] = new RunMigrationsOnConnect( |
190 | function () { |
191 | $this->runMigrations($this->createdEntityManager); |
192 | } |
193 | ); |
194 | } |
195 | $config->setMiddlewares($middlewares); |
196 | if (!$this->createdEntityManager || !$this->createdEntityManager->isOpen()) { |
197 | $connection = DriverManager::getConnection($this->connectionConfig, $config); |
198 | $eventManager = new EventManager(); |
199 | $this->createdEntityManager = new EntityManager($connection, $config, $eventManager); |
200 | } |
201 | |
202 | return $this->createdEntityManager; |
203 | } |
204 | } |