Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 101
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
SessionDatalayer
0.00% covered (danger)
0.00%
0 / 101
0.00% covered (danger)
0.00%
0 / 11
552
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 restore
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 store
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 all
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 persistNew
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
20
 persistExisting
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
 find
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 getMetadata
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 removeExisting
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 upsert
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 storeMetadata
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2namespace Apie\Core\Datalayers\InMemory;
3
4use Apie\Core\ApieLib;
5use Apie\Core\BoundedContext\BoundedContextId;
6use Apie\Core\Datalayers\ApieDatalayer;
7use Apie\Core\Datalayers\DataLayerMetadata;
8use Apie\Core\Datalayers\Lists\EntityListInterface;
9use Apie\Core\Datalayers\Lists\InMemoryEntityList;
10use Apie\Core\Datalayers\Search\LazyLoadedListFilterer;
11use Apie\Core\Entities\EntityInterface;
12use Apie\Core\Exceptions\EntityAlreadyPersisted;
13use Apie\Core\Exceptions\EntityNotFoundException;
14use Apie\Core\Exceptions\UnknownExistingEntityError;
15use Apie\Core\Identifiers\AutoIncrementInteger;
16use Apie\Core\Identifiers\IdentifierInterface;
17use Faker\Factory;
18use Faker\Generator;
19use ReflectionClass;
20use ReflectionProperty;
21use Symfony\Component\HttpFoundation\Session\SessionInterface;
22
23class SessionDatalayer implements ApieDatalayer
24{
25    /**
26     * @var array<string, array<string, array<int, EntityInterface>>>
27     */
28    private array $stored = [];
29
30    /**
31     * @var array<string, array<class-string<EntityInterface>, EntityListInterface<EntityInterface>>>
32     */
33    private array $alreadyLoadedLists = [];
34
35    private Generator $generator;
36
37    /** @var array<string, array<class-string<EntityInterface>, array<string, DataLayerMetadata>>> */
38    private array $metadata = [];
39
40    public function __construct(
41        private readonly SessionInterface $session,
42        private readonly LazyLoadedListFilterer $filterer,
43        private readonly string $sessionKey = 'datalayer'
44    ) {
45        $this->generator = Factory::create();
46        $this->stored = $this->restore();
47        $this->metadata = $this->session->get($this->sessionKey . '_metadata', []);
48    }
49
50    /**
51     * @return array<string, array<string, array<int, EntityInterface>>>
52     */
53    protected function restore(): array
54    {
55        return $this->session->get($this->sessionKey, []);
56    }
57
58    /**
59     * @param array<string, array<string, array<int, EntityInterface>>> $list
60     */
61    protected function store(array $list): void
62    {
63        $this->session->set($this->sessionKey, $list);
64    }
65
66    public function all(ReflectionClass $class, ?BoundedContextId $boundedContextId = null): EntityListInterface
67    {
68        $boundedContextId ??= new BoundedContextId('unknown');
69        $boundedContextIdString = $boundedContextId->toNative();
70        $className = $class->name;
71        $this->stored[$boundedContextIdString][$className] ??= [];
72        if (!isset($this->alreadyLoadedLists[$boundedContextIdString][$className])) {
73            $this->alreadyLoadedLists[$boundedContextIdString][$className] = new InMemoryEntityList(
74                $class,
75                $boundedContextId,
76                $this->filterer,
77                $this->stored[$boundedContextIdString][$className]
78            );
79        }
80        return $this->alreadyLoadedLists[$boundedContextIdString][$className];
81    }
82
83    /**
84     * @template T of EntityInterface
85     * @param T $entity
86     * @return T
87     */
88    public function persistNew(EntityInterface $entity, ?BoundedContextId $boundedContextId = null): EntityInterface
89    {
90        $boundedContextId ??= new BoundedContextId('unknown');
91        $boundedContextIdString = $boundedContextId->toNative();
92        $id = $entity->getId();
93        if ($id instanceof AutoIncrementInteger) {
94            $id = $id::createRandom($this->generator);
95            $reflProperty = new ReflectionProperty($entity, 'id');
96            $reflProperty->setValue($entity, $id);
97        }
98        $className = $id::getReferenceFor()->name;
99        $id = $entity->getId()->toNative();
100        foreach ($this->stored[$boundedContextIdString][$className] ?? [] as $entityInList) {
101            if ($entityInList->getId()->toNative() === $id) {
102                throw new EntityAlreadyPersisted($entity);
103            }
104        }
105        $this->stored[$boundedContextIdString][$className][] = $entity;
106        $this->store($this->stored);
107        $now = ApieLib::getPsrClock()->now();
108        $this->metadata[$boundedContextIdString][$className][(string) $id] = new DataLayerMetadata(
109            $now,
110            $now,
111            ApieLib::VERSION,
112            ApieLib::VERSION
113        );
114        $this->storeMetadata();
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        $boundedContextId ??= new BoundedContextId('unknown');
126        $boundedContextIdString = $boundedContextId->toNative();
127        $id = $entity->getId()->toNative();
128        $className = $entity->getId()::getReferenceFor()->name;
129        foreach ($this->stored[$boundedContextIdString][$className] ?? [] as $key => $entityInList) {
130            if ($entityInList->getId()->toNative() === $id) {
131                $this->stored[$boundedContextIdString][$className][$key] = $entity;
132                $this->store($this->stored);
133                $metadata = $this->metadata[$boundedContextIdString][$className][(string) $id] ?? new DataLayerMetadata(
134                    ApieLib::getPsrClock()->now(),
135                    ApieLib::getPsrClock()->now(),
136                    ApieLib::VERSION,
137                    ApieLib::VERSION
138                );
139                $this->metadata[$boundedContextIdString][$className][(string) $id] = $metadata->withUpdatedMetadata(
140                    ApieLib::getPsrClock()->now(),
141                    ApieLib::VERSION
142                );
143                $this->storeMetadata();
144                return $entity;
145            }
146        }
147        throw new UnknownExistingEntityError($entity);
148    }
149
150    public function find(IdentifierInterface $identifier, ?BoundedContextId $boundedContextId = null): EntityInterface
151    {
152        $boundedContextId ??= new BoundedContextId('unknown');
153        $boundedContextIdString = $boundedContextId->toNative();
154        $className = $identifier::getReferenceFor()->name;
155        $id = $identifier->toNative();
156        foreach ($this->stored[$boundedContextIdString][$className] ?? [] as $entityInList) {
157            if ($entityInList->getId()->toNative() === $id) {
158                return $entityInList;
159            }
160        }
161        throw new EntityNotFoundException($identifier);
162    }
163
164    /**
165     * @template T of EntityInterface
166     * @param IdentifierInterface<T> $identifier
167     */
168    public function getMetadata(
169        IdentifierInterface $identifier,
170        ?BoundedContextId $boundedContextId = null
171    ): DataLayerMetadata {
172        $boundedContextId ??= new BoundedContextId('unknown');
173        $this->find($identifier, $boundedContextId);
174        $contextId = $boundedContextId->toNative();
175        $className = $identifier::getReferenceFor()->name;
176        $id = (string) $identifier->toNative();
177        if (!isset($this->metadata[$contextId][$className][$id])) {
178            $now = ApieLib::getPsrClock()->now();
179            $this->metadata[$contextId][$className][$id] = new DataLayerMetadata(
180                $now,
181                $now,
182                ApieLib::VERSION,
183                ApieLib::VERSION
184            );
185            $this->storeMetadata();
186        }
187
188        return $this->metadata[$contextId][$className][$id];
189    }
190
191    public function removeExisting(EntityInterface $entity, ?BoundedContextId $boundedContextId = null): void
192    {
193        $boundedContextId ??= new BoundedContextId('unknown');
194        $boundedContextIdString = $boundedContextId->toNative();
195        $identifier = $entity->getId();
196        $className = $identifier::getReferenceFor()->name;
197        $id = $identifier->toNative();
198        $newList = [];
199        foreach ($this->stored[$boundedContextIdString][$className] ?? [] as $entityInList) {
200            if ($entityInList->getId()->toNative() !== $id) {
201                $newList[] = $entityInList;
202            }
203        }
204        $this->stored[$boundedContextIdString][$className] = $newList;
205        unset($this->metadata[$boundedContextIdString][$className][(string) $id]);
206        $this->store($this->stored);
207        $this->storeMetadata();
208    }
209
210    public function upsert(EntityInterface $entity, ?BoundedContextId $boundedContextId): EntityInterface
211    {
212        try {
213            return $this->persistExisting($entity, $boundedContextId);
214        } catch (UnknownExistingEntityError) {
215            return $this->persistNew($entity, $boundedContextId);
216        }
217    }
218
219    private function storeMetadata(): void
220    {
221        $this->session->set($this->sessionKey . '_metadata', $this->metadata);
222    }
223}