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