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