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