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