Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
63.33% covered (warning)
63.33%
57 / 90
28.57% covered (danger)
28.57%
2 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
OrmBuilder
63.33% covered (warning)
63.33%
57 / 90
28.57% covered (danger)
28.57%
2 / 7
90.99
0.00% covered (danger)
0.00%
0 / 1
 __construct
20.00% covered (danger)
20.00%
2 / 10
0.00% covered (danger)
0.00%
0 / 1
24.43
 getGeneratedNamespace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLogEntity
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 runMigrations
40.91% covered (danger)
40.91%
9 / 22
0.00% covered (danger)
0.00%
0 / 1
17.11
 toDoctrineClass
53.85% covered (warning)
53.85%
7 / 13
0.00% covered (danger)
0.00%
0 / 1
5.57
 isEmptyPath
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 createEntityManager
85.71% covered (warning)
85.71%
30 / 35
0.00% covered (danger)
0.00%
0 / 1
10.29
1<?php
2namespace Apie\DoctrineEntityDatalayer;
3
4use Apie\Core\BoundedContext\BoundedContextId;
5use Apie\Core\Entities\EntityInterface;
6use Apie\DoctrineEntityConverter\OrmBuilder as DoctrineEntityConverterOrmBuilder;
7use Apie\DoctrineEntityDatalayer\Exceptions\CouldNotUpdateDatabaseAutomatically;
8use Apie\DoctrineEntityDatalayer\Middleware\RunMigrationsOnConnect;
9use Apie\StorageMetadata\Interfaces\StorageDtoInterface;
10use Apie\StorageMetadataBuilder\Interfaces\RootObjectInterface;
11use Doctrine\Bundle\DoctrineBundle\Middleware\DebugMiddleware;
12use Doctrine\Common\EventManager;
13use Doctrine\DBAL\DriverManager;
14use Doctrine\DBAL\Exception\DriverException;
15use Doctrine\DBAL\Exception\MalformedDsnException;
16use Doctrine\DBAL\Schema\AbstractAsset;
17use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory;
18use Doctrine\DBAL\Tools\DsnParser;
19use Doctrine\ORM\EntityManager;
20use Doctrine\ORM\EntityManagerInterface;
21use Doctrine\ORM\ORMSetup;
22use Doctrine\ORM\Tools\SchemaTool;
23use FilesystemIterator;
24use Psr\Cache\CacheItemPoolInterface;
25use RecursiveDirectoryIterator;
26use ReflectionClass;
27use RuntimeException;
28
29class 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<covariant EntityInterface> $class
121     * @return ReflectionClass<covariant 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            /** @var ReflectionClass<covariant StorageDtoInterface> $refl */
128            $refl = new ReflectionClass($metadata->getName());
129            if (in_array(RootObjectInterface::class, $refl->getInterfaceNames())) {
130                $originalClass = $refl->getMethod('getClassReference')->invoke(null);
131                if ($originalClass->name === $class->name) {
132                    return $refl;
133                }
134            }
135        }
136        throw new RuntimeException(
137            sprintf(
138                'Could not find Doctrine class to handle %s',
139                $class->name
140            )
141        );
142    }
143
144    private function isEmptyPath(): bool
145    {
146        if (!file_exists($this->path) || !is_dir($this->path)) {
147            return true;
148        }
149        $di = new RecursiveDirectoryIterator($this->path, FilesystemIterator::SKIP_DOTS);
150        foreach ($di as $ignored) {
151            return false;
152        }
153
154        return true;
155    }
156
157    public function createEntityManager(): EntityManagerInterface
158    {
159        $this->isModified = false;
160        if (!$this->buildOnce || $this->isEmptyPath()) {
161            $this->isModified = $this->ormBuilder->createOrm($this->path);
162            $this->buildOnce = true;
163        }
164        $path = $this->path . '/build' . $this->ormBuilder->getLastGeneratedCode($this->path)->getId();
165
166        $config = ORMSetup::createAttributeMetadataConfiguration(
167            [$path],
168            $this->devMode,
169            $this->proxyDir,
170            $this->devMode ? null : $this->cache
171        );
172        $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory());
173        $config->setLazyGhostObjectEnabled(true);
174        $config->setSchemaAssetsFilter(static function (string|AbstractAsset $assetName): bool {
175            if ($assetName instanceof AbstractAsset) {
176                $assetName = $assetName->getName();
177            }
178
179            if ($assetName === 'doctrine_migration_versions') {
180                return true;
181            }
182        
183            return (bool) preg_match("~^apie_~i", $assetName);
184        });
185        $middlewares = [];
186        if ($this->debugMiddleware) {
187            $middlewares[] = $this->debugMiddleware;
188        }
189        if ($this->runMigrations) {
190            $middlewares[] = new RunMigrationsOnConnect(
191                function () {
192                    $this->runMigrations($this->createdEntityManager);
193                }
194            );
195        }
196        $config->setMiddlewares($middlewares);
197        if (!$this->createdEntityManager || !$this->createdEntityManager->isOpen()) {
198            $connection = DriverManager::getConnection($this->connectionConfig, $config);
199            $eventManager = new EventManager();
200            $this->createdEntityManager = new EntityManager($connection, $config, $eventManager);
201        }
202        
203        return $this->createdEntityManager;
204    }
205}