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