Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.33% covered (success)
97.33%
73 / 75
80.00% covered (warning)
80.00%
8 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
InMemoryDatalayer
97.33% covered (success)
97.33%
73 / 75
80.00% covered (warning)
80.00%
8 / 10
24
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 restore
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 store
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 all
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 persistNew
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
7
 persistExisting
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 find
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getMetadata
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 removeExisting
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 upsert
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2namespace Apie\Core\Datalayers\InMemory;
3
4use Apie\Core\ApieLib;
5use Apie\Core\Attributes\ClassStoreOptions;
6use Apie\Core\BoundedContext\BoundedContextId;
7use Apie\Core\Datalayers\ApieDatalayer;
8use Apie\Core\Datalayers\DataLayerMetadata;
9use Apie\Core\Datalayers\Lists\EntityListInterface;
10use Apie\Core\Datalayers\Lists\InMemoryEntityList;
11use Apie\Core\Datalayers\Search\LazyLoadedListFilterer;
12use Apie\Core\Entities\EntityInterface;
13use Apie\Core\Enums\SortingOrder;
14use Apie\Core\Exceptions\EntityAlreadyPersisted;
15use Apie\Core\Exceptions\EntityNotFoundException;
16use Apie\Core\Exceptions\UnknownExistingEntityError;
17use Apie\Core\Identifiers\AutoIncrementInteger;
18use Apie\Core\Identifiers\IdentifierInterface;
19use Faker\Factory;
20use Faker\Generator;
21use ReflectionClass;
22use ReflectionProperty;
23use WeakMap;
24
25class InMemoryDatalayer implements ApieDatalayer
26{
27    /**
28     * @var array<string, array<int, EntityInterface>>
29     */
30    private array $stored = [];
31
32    /**
33     * @var array<class-string<EntityInterface>, EntityListInterface<EntityInterface>>
34     */
35    private array $alreadyLoadedLists = [];
36
37    private Generator $generator;
38
39    /** @var WeakMap<EntityInterface, DataLayerMetadata> */
40    private WeakMap $metadata;
41
42    public function __construct(private BoundedContextId $boundedContextId, private LazyLoadedListFilterer $filterer)
43    {
44        $this->generator = Factory::create();
45        $this->metadata = new WeakMap();
46        $this->stored = $this->restore();
47    }
48
49    /**
50     * @return array<string, array<int, EntityInterface>>
51     */
52    protected function restore(): array
53    {
54        return [];
55    }
56
57    /**
58     * @param array<string, array<int, EntityInterface>> $list
59     */
60    protected function store(array $list): void
61    {
62    }
63
64    public function all(ReflectionClass $class, ?BoundedContextId $boundedContextId = null): EntityListInterface
65    {
66        $className = $class->name;
67        $this->stored[$className] ??= [];
68        if (!isset($this->alreadyLoadedLists[$className])) {
69            $this->alreadyLoadedLists[$className] = new InMemoryEntityList(
70                $class,
71                $this->boundedContextId,
72                $this->filterer,
73                $this->stored[$className]
74            );
75        }
76        return $this->alreadyLoadedLists[$className];
77    }
78
79    /**
80     * @template T of EntityInterface
81     * @param T $entity
82     * @return T
83     */
84    public function persistNew(EntityInterface $entity, ?BoundedContextId $boundedContextId = null): EntityInterface
85    {
86        $id = $entity->getId();
87        if ($id instanceof AutoIncrementInteger) {
88            $id = $id::createRandom($this->generator);
89            $reflProperty = new ReflectionProperty($entity, 'id');
90            $reflProperty->setValue($entity, $id);
91        }
92        $refl = $id::getReferenceFor();
93        $className = $refl->name;
94        $id = $entity->getId()->toNative();
95        $this->stored[$className] ??= [];
96        foreach ($this->stored[$className] as $entityInList) {
97            if ($entityInList->getId()->toNative() === $id) {
98                throw new EntityAlreadyPersisted($entity);
99            }
100        }
101        $sortOrder = SortingOrder::Descending;
102        foreach ($refl->getAttributes(ClassStoreOptions::class) as $classStoreOptionsAttribute) {
103            $classStoreOptions = $classStoreOptionsAttribute->newInstance();
104            if ($classStoreOptions->defaultSortingOrder === SortingOrder::Ascending) {
105                $sortOrder = SortingOrder::Ascending;
106            }
107        }
108        if ($sortOrder === SortingOrder::Ascending) {
109            $this->stored[$className][] = $entity;
110        } else {
111            array_unshift($this->stored[$className], $entity);
112        }
113        $now = ApieLib::getPsrClock()->now();
114        $this->metadata[$entity] = new DataLayerMetadata($now, $now, ApieLib::VERSION, ApieLib::VERSION);
115        return $entity;
116    }
117
118    /**
119     * @template T of EntityInterface
120     * @param T $entity
121     * @return T
122     */
123    public function persistExisting(EntityInterface $entity, ?BoundedContextId $boundedContextId = null): EntityInterface
124    {
125        $id = $entity->getId()->toNative();
126        $className = $entity->getId()::getReferenceFor()->name;
127        foreach ($this->stored[$className] ?? [] as $key => $entityInList) {
128            if ($entityInList->getId()->toNative() === $id) {
129                $metadata = $this->metadata[$entityInList] ?? new DataLayerMetadata(
130                    ApieLib::getPsrClock()->now(),
131                    ApieLib::getPsrClock()->now(),
132                    ApieLib::VERSION,
133                    ApieLib::VERSION
134                );
135                $this->stored[$className][$key] = $entity;
136                $this->metadata[$entity] = $metadata->withUpdatedMetadata(
137                    ApieLib::getPsrClock()->now(),
138                    ApieLib::VERSION
139                );
140                return $entity;
141            }
142        }
143        throw new UnknownExistingEntityError($entity);
144    }
145
146    public function find(IdentifierInterface $identifier, ?BoundedContextId $boundedContextId = null): EntityInterface
147    {
148        $className = $identifier::getReferenceFor()->name;
149        $id = $identifier->toNative();
150        foreach ($this->stored[$className] ?? [] as $entityInList) {
151            if ($entityInList->getId()->toNative() === $id) {
152                return $entityInList;
153            }
154        }
155        throw new EntityNotFoundException($identifier);
156    }
157
158    /**
159     * @template T of EntityInterface
160     * @param IdentifierInterface<T> $identifier
161     */
162    public function getMetadata(
163        IdentifierInterface $identifier,
164        ?BoundedContextId $boundedContextId = null
165    ): DataLayerMetadata {
166        $entity = $this->find($identifier, $boundedContextId);
167        return $this->metadata[$entity] ?? throw new \LogicException('Metadata is not available for this entity.');
168    }
169
170    public function removeExisting(EntityInterface $entity, ?BoundedContextId $boundedContextId = null): void
171    {
172        $identifier = $entity->getId();
173        $className = $identifier::getReferenceFor()->name;
174        $id = $identifier->toNative();
175        $newList = [];
176        foreach ($this->stored[$className] ?? [] as $entityInList) {
177            if ($entityInList->getId()->toNative() !== $id) {
178                $newList[] = $entityInList;
179            } else {
180                unset($this->metadata[$entityInList]);
181            }
182        }
183        $this->stored[$className] = $newList;
184    }
185
186    public function upsert(EntityInterface $entity, ?BoundedContextId $boundedContextId): EntityInterface
187    {
188        try {
189            return $this->persistExisting($entity, $boundedContextId);
190        } catch (UnknownExistingEntityError) {
191            return $this->persistNew($entity, $boundedContextId);
192        }
193    }
194}