Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
9 / 9
CRAP
100.00% covered (success)
100.00%
1 / 1
InMemoryLogisticsObjectStore
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
9 / 9
18
100.00% covered (success)
100.00%
1 / 1
 latest
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 revision
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 at
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 exists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 saveRevision
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 snapshot
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 erase
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 stored
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace LambdaTwelve\OneRecord\Server\InMemory;
6
7use DateTimeImmutable;
8use LambdaTwelve\OneRecord\Model\LogisticsObject;
9use LambdaTwelve\OneRecord\Rdf\Graph;
10use LambdaTwelve\OneRecord\Rdf\Iri;
11use LambdaTwelve\OneRecord\Server\Spi\LogisticsObjectStore;
12use LambdaTwelve\OneRecord\Server\Spi\StoredObject;
13use LambdaTwelve\OneRecord\Server\Spi\StoreException;
14use LambdaTwelve\OneRecord\Server\Spi\Volatile;
15
16/**
17 * Every revision of every object in arrays. The reference implementation for
18 * tests and bin/serve, and the behavioural contract a database-backed store
19 * must match (see LogisticsObjectStoreContractTest).
20 */
21final class InMemoryLogisticsObjectStore implements LogisticsObjectStore, Volatile
22{
23    /** @var array<string, list<array{object: LogisticsObject, at: DateTimeImmutable}>> IRI => revisions, index 0 = revision 1 */
24    private array $revisions = [];
25
26    public function latest(Iri $iri): ?StoredObject
27    {
28        $revisions = $this->revisions[$iri->value] ?? [];
29
30        return $revisions === [] ? null : $this->stored($iri, \count($revisions));
31    }
32
33    public function revision(Iri $iri, int $revision): ?StoredObject
34    {
35        $revisions = $this->revisions[$iri->value] ?? [];
36
37        return $revision >= 1 && $revision <= \count($revisions) ? $this->stored($iri, $revision) : null;
38    }
39
40    public function at(Iri $iri, DateTimeImmutable $at): ?StoredObject
41    {
42        $revisions = $this->revisions[$iri->value] ?? [];
43        $found = null;
44        foreach ($revisions as $index => $entry) {
45            if ($entry['at'] <= $at) {
46                $found = $index + 1;
47            }
48        }
49
50        return $found === null ? null : $this->stored($iri, $found);
51    }
52
53    public function exists(Iri $iri): bool
54    {
55        return isset($this->revisions[$iri->value]);
56    }
57
58    public function create(LogisticsObject $object, DateTimeImmutable $at): StoredObject
59    {
60        if (isset($this->revisions[$object->iri->value])) {
61            throw StoreException::alreadyExists($object->iri);
62        }
63        $this->revisions[$object->iri->value] = [['object' => self::snapshot($object), 'at' => $at]];
64
65        return $this->stored($object->iri, 1);
66    }
67
68    public function saveRevision(LogisticsObject $object, int $expectedCurrent, DateTimeImmutable $at): StoredObject
69    {
70        $revisions = $this->revisions[$object->iri->value] ?? null;
71        if ($revisions === null) {
72            throw StoreException::notFound($object->iri);
73        }
74        if (\count($revisions) !== $expectedCurrent) {
75            throw StoreException::revisionConflict($object->iri, $expectedCurrent, \count($revisions));
76        }
77        $this->revisions[$object->iri->value][] = ['object' => self::snapshot($object), 'at' => $at];
78
79        return $this->stored($object->iri, $expectedCurrent + 1);
80    }
81
82    /**
83     * Graph is mutable by design (builders and the applier edit one); a stored
84     * revision must not change when a caller keeps editing the object it passed
85     * in or the one it read back (AR-015). A database store gets this for free
86     * by serialising; the reference store copies.
87     */
88    private static function snapshot(LogisticsObject $object): LogisticsObject
89    {
90        return $object->withGraph(new Graph($object->graph));
91    }
92
93    public function erase(Iri $iri): void
94    {
95        unset($this->revisions[$iri->value]);
96    }
97
98    private function stored(Iri $iri, int $revision): StoredObject
99    {
100        $revisions = $this->revisions[$iri->value];
101        $entry = $revisions[$revision - 1];
102
103        return new StoredObject(self::snapshot($entry['object']), $revision, \count($revisions), $revisions[0]['at'], $entry['at']);
104    }
105}