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