Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.20% covered (warning)
87.20%
109 / 125
63.64% covered (warning)
63.64%
7 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
DoctrineEntityDatalayer
87.20% covered (warning)
87.20%
109 / 125
63.64% covered (warning)
63.64%
7 / 11
36.42
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEntityManager
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 toDoctrineClass
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getOrderByColumns
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
5
 getFilterColumns
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 all
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 find
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 persistNew
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
4.02
 persistExisting
76.19% covered (warning)
76.19%
16 / 21
0.00% covered (danger)
0.00%
0 / 1
4.22
 removeExisting
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
2.01
 upsert
66.67% covered (warning)
66.67%
16 / 24
0.00% covered (danger)
0.00%
0 / 1
4.59
1<?php
2namespace Apie\DoctrineEntityDatalayer;
3
4use Apie\Core\Attributes\OverwriteAfterPersist;
5use Apie\Core\BoundedContext\BoundedContextId;
6use Apie\Core\Context\ApieContext;
7use Apie\Core\Datalayers\ApieDatalayerWithFilters;
8use Apie\Core\Datalayers\Lists\EntityListInterface;
9use Apie\Core\Entities\EntityInterface;
10use Apie\Core\Entities\RequiresRecalculatingInterface;
11use Apie\Core\Enums\ScalarType;
12use Apie\Core\Exceptions\EntityNotFoundException;
13use Apie\Core\Identifiers\IdentifierInterface;
14use Apie\Core\Lists\StringSet;
15use Apie\Core\Metadata\MetadataFactory;
16use Apie\Core\TypeUtils;
17use Apie\DoctrineEntityDatalayer\Exceptions\InsertConflict;
18use Apie\DoctrineEntityDatalayer\Factories\DoctrineListFactory;
19use Apie\DoctrineEntityDatalayer\Factories\EntityQueryFilterFactory;
20use Apie\DoctrineEntityDatalayer\IndexStrategy\IndexStrategyInterface;
21use Apie\StorageMetadata\Attributes\GetSearchIndexAttribute;
22use Apie\StorageMetadata\Attributes\PropertyAttribute;
23use Apie\StorageMetadata\DomainToStorageConverter;
24use Apie\StorageMetadata\Interfaces\StorageDtoInterface;
25use Apie\StorageMetadataBuilder\Interfaces\HasIndexInterface;
26use Apie\StorageMetadataBuilder\Interfaces\RootObjectInterface;
27use Apie\TypeConverter\ReflectionTypeFactory;
28use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
29use Doctrine\ORM\EntityManagerInterface;
30use Doctrine\ORM\Exception\EntityIdentityCollisionException;
31use ReflectionAttribute;
32use ReflectionClass;
33use ReflectionProperty;
34
35class DoctrineEntityDatalayer implements ApieDatalayerWithFilters
36{
37    private ?EntityManagerInterface $entityManager = null;
38
39    private bool $logged = false;
40
41    public function __construct(
42        private readonly OrmBuilder $ormBuilder,
43        private readonly DomainToStorageConverter $domainToStorageConverter,
44        private readonly IndexStrategyInterface $entityReindexer,
45        private readonly DoctrineListFactory $doctrineListFactory
46    ) {
47    }
48
49    private function getEntityManager(): EntityManagerInterface
50    {
51        if (!isset($this->entityManager) || !$this->entityManager->isOpen()) {
52            $this->entityManager = $this->ormBuilder->createEntityManager();
53            $log = $this->ormBuilder->getLogEntity();
54            if ($log) {
55                $this->upsert($log, new BoundedContextId('core'));
56            }
57        }
58
59        return $this->entityManager;
60    }
61
62    /**
63     * @param ReflectionClass<EntityInterface> $class
64     * @return ReflectionClass<StorageDtoInterface>
65     */
66    public function toDoctrineClass(ReflectionClass $class, ?BoundedContextId $boundedContextId = null): ReflectionClass
67    {
68        $doctrineEntityClass = $this->ormBuilder->toDoctrineClass($class, $boundedContextId);
69        if (!$this->logged) {
70            $log = $this->ormBuilder->getLogEntity();
71            if ($log) {
72                $this->upsert($log, new BoundedContextId('core'));
73            }
74            $this->logged = true;
75        }
76        return $doctrineEntityClass;
77    }
78
79    /**
80     * @see EntityQueryFilterFactory
81     */
82    public function getOrderByColumns(ReflectionClass $class, BoundedContextId $boundedContextId): ?StringSet
83    {
84        $doctrineEntityClass = $this->toDoctrineClass($class, $boundedContextId);
85        $list = [];
86        if (in_array(RequiresRecalculatingInterface::class, $class->getInterfaceNames())) {
87            $list[] = 'dateToRecalculate';
88        }
89        $list[] = 'createdAt';
90        $list[] = 'updatedAt';
91        foreach ($doctrineEntityClass->getProperties(ReflectionProperty::IS_PUBLIC) as $publicProperty) {
92            foreach ($publicProperty->getAttributes(PropertyAttribute::class, ReflectionAttribute::IS_INSTANCEOF) as $publicPropertyAttribute) {
93                $metadata = MetadataFactory::getModificationMetadata(
94                    $publicProperty->getType() ?? ReflectionTypeFactory::createReflectionType('mixed'),
95                    new ApieContext()
96                );
97
98                if (in_array($metadata->toScalarType(true), ScalarType::PRIMITIVES)) {
99                    $list[] = $publicPropertyAttribute->newInstance()->propertyName;
100                }
101                break;
102            }
103        }
104        return new StringSet($list);
105    }
106
107    public function getFilterColumns(ReflectionClass $class, BoundedContextId $boundedContextId): StringSet
108    {
109        $doctrineEntityClass = $this->ormBuilder->toDoctrineClass($class);
110        $list = [];
111        foreach ($doctrineEntityClass->getProperties(ReflectionProperty::IS_PUBLIC) as $publicProperty) {
112            if (str_starts_with($publicProperty->name, 'search_')) {
113                foreach ($publicProperty->getAttributes(GetSearchIndexAttribute::class) as $publicPropertyAttribute) {
114                    $list[] = substr($publicProperty->name, strlen('search_'));
115                }
116            }
117        }
118        return new StringSet($list);
119    }
120
121    public function all(ReflectionClass $class, ?BoundedContextId $boundedContextId = null): EntityListInterface
122    {
123        return $this->doctrineListFactory->createFor(
124            $class,
125            $this->toDoctrineClass($class, $boundedContextId),
126            $boundedContextId ?? new BoundedContextId('unknown')
127        );
128    }
129
130    public function find(IdentifierInterface $identifier, ?BoundedContextId $boundedContextId = null): EntityInterface
131    {
132        $entityManager = $this->getEntityManager();
133        $domainClass = $identifier->getReferenceFor();
134        $doctrineEntityClass = $this->toDoctrineClass($domainClass, $boundedContextId)->name;
135        $doctrineEntity = $entityManager->find($doctrineEntityClass, $identifier->toNative());
136        if (!($doctrineEntity instanceof StorageDtoInterface)) {
137            throw new EntityNotFoundException($identifier);
138        }
139        assert($doctrineEntity instanceof RootObjectInterface);
140        DoctrineUtils::loadAllProxies($doctrineEntity);
141        return $this->domainToStorageConverter->createDomainObject($doctrineEntity);
142    }
143    
144    public function persistNew(EntityInterface $entity, ?BoundedContextId $boundedContextId = null): EntityInterface
145    {
146        $entityManager = $this->getEntityManager();
147        $identifier = $entity->getId();
148        $domainClass = $identifier->getReferenceFor();
149        /** @var class-string<StorageDtoInterface> $doctrineEntityClass */
150        $doctrineEntityClass = $this->toDoctrineClass($domainClass, $boundedContextId)->name;
151        $doctrineEntity = $this->domainToStorageConverter->createStorageObject(
152            $entity,
153            new ReflectionClass($doctrineEntityClass)
154        );
155
156        try {
157            $entityManager->persist($doctrineEntity);
158            $entityManager->flush();
159        } catch (UniqueConstraintViolationException|EntityIdentityCollisionException $uniqueConstraintViolation) {
160            throw new InsertConflict($uniqueConstraintViolation);
161        }
162        if (TypeUtils::findAttributes(new ReflectionClass($entity), OverwriteAfterPersist::class)) {
163            $this->domainToStorageConverter->injectExistingDomainObject(
164                $entity,
165                $doctrineEntity
166            );
167        }
168        if ($doctrineEntity instanceof HasIndexInterface) {
169            $this->entityReindexer->updateIndex($doctrineEntity, $entity);
170        }
171        
172        return $entity;
173    }
174    
175    public function persistExisting(EntityInterface $entity, ?BoundedContextId $boundedContextId = null): EntityInterface
176    {
177        $entityManager = $this->getEntityManager();
178        $identifier = $entity->getId();
179        $domainClass = $identifier->getReferenceFor();
180        $doctrineEntityClass = $this->toDoctrineClass($domainClass, $boundedContextId)->name;
181        /** @var (StorageDtoInterface&RootObjectInterface)|null $doctrineEntity */
182        $doctrineEntity = $entityManager->find($doctrineEntityClass, $identifier->toNative());
183        if (!$doctrineEntity) {
184            throw new EntityNotFoundException($identifier);
185        }
186        $this->domainToStorageConverter->injectExistingStorageObject(
187            $entity,
188            $doctrineEntity
189        );
190        $entityManager->persist($doctrineEntity);
191        $entityManager->flush();
192        if (TypeUtils::findAttributes(new ReflectionClass($entity), OverwriteAfterPersist::class)) {
193            $this->domainToStorageConverter->injectExistingDomainObject(
194                $entity,
195                $doctrineEntity
196            );
197        }
198        if ($doctrineEntity instanceof HasIndexInterface) {
199            $this->entityReindexer->updateIndex($doctrineEntity, $entity);
200        }
201        return $entity;
202    }
203
204    public function removeExisting(EntityInterface $entity, ?BoundedContextId $boundedContextId = null): void
205    {
206        $entityManager = $this->getEntityManager();
207        $identifier = $entity->getId();
208        $domainClass = $identifier->getReferenceFor();
209        $doctrineEntityClass = $this->toDoctrineClass($domainClass, $boundedContextId)->name;
210        /** @var (StorageDtoInterface&RootObjectInterface)|null $doctrineEntity */
211        $doctrineEntity = $entityManager->find($doctrineEntityClass, $identifier->toNative());
212        if (!$doctrineEntity) {
213            throw new EntityNotFoundException($identifier);
214        }
215        $entityManager->remove($doctrineEntity);
216        $entityManager->flush();
217    }
218
219    public function upsert(EntityInterface $entity, ?BoundedContextId $boundedContextId): EntityInterface
220    {
221        $entityManager = $this->getEntityManager();
222        $identifier = $entity->getId();
223        $domainClass = $identifier->getReferenceFor();
224        $doctrineEntityClass = $this->toDoctrineClass($domainClass, $boundedContextId);
225        /** @var (StorageDtoInterface&RootObjectInterface)|null $doctrineEntity */
226        $doctrineEntity = $entityManager->find($doctrineEntityClass->name, $identifier->toNative());
227        if ($doctrineEntity) {
228            $this->domainToStorageConverter->injectExistingStorageObject(
229                $entity,
230                $doctrineEntity
231            );
232        } else {
233            $doctrineEntity = $this->domainToStorageConverter->createStorageObject(
234                $entity,
235                $doctrineEntityClass
236            );
237        }
238        $entityManager->persist($doctrineEntity);
239        $entityManager->flush();
240        if (TypeUtils::findAttributes(new ReflectionClass($entity), OverwriteAfterPersist::class)) {
241            $this->domainToStorageConverter->injectExistingDomainObject(
242                $entity,
243                $doctrineEntity
244            );
245        }
246        if ($doctrineEntity instanceof HasIndexInterface) {
247            $this->entityReindexer->updateIndex($doctrineEntity, $entity);
248        }
249        return $entity;
250    }
251}