Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.37% covered (success)
97.37%
37 / 38
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
InMemoryLogisticsEventStore
97.37% covered (success)
97.37%
37 / 38
83.33% covered (warning)
83.33%
5 / 6
27
0.00% covered (danger)
0.00%
0 / 1
 append
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 eraseFor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
4 / 4
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
 query
96.15% covered (success)
96.15%
25 / 26
0.00% covered (danger)
0.00%
0 / 1
17
 lastModified
100.00% covered (success)
100.00%
1 / 1
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\LogisticsEvent;
9use LambdaTwelve\OneRecord\Rdf\Graph;
10use LambdaTwelve\OneRecord\Rdf\Iri;
11use LambdaTwelve\OneRecord\Server\Spi\EventQuery;
12use LambdaTwelve\OneRecord\Server\Spi\LogisticsEventStore;
13use LambdaTwelve\OneRecord\Server\Spi\StoreException;
14use LambdaTwelve\OneRecord\Server\Spi\Volatile;
15
16final class InMemoryLogisticsEventStore implements LogisticsEventStore, Volatile
17{
18    /** @var array<string, array<string, LogisticsEvent>> object IRI => event IRI => event */
19    private array $events = [];
20
21    /** @var array<string, DateTimeImmutable> */
22    private array $lastModified = [];
23
24    public function append(LogisticsEvent $event): void
25    {
26        if ($this->get($event->iri) !== null) {
27            // Uniqueness at the scope get() looks up: the whole server, not one object's bucket (AR-022).
28            throw StoreException::alreadyExists($event->iri);
29        }
30        $this->events[$event->logisticsObject->value][$event->iri->value] = self::snapshot($event);
31        $current = $this->lastModified[$event->logisticsObject->value] ?? null;
32        $this->lastModified[$event->logisticsObject->value] = $current === null || $event->created > $current ? $event->created : $current;
33    }
34
35    public function eraseFor(Iri $logisticsObject): void
36    {
37        unset($this->events[$logisticsObject->value], $this->lastModified[$logisticsObject->value]);
38    }
39
40    public function get(Iri $eventIri): ?LogisticsEvent
41    {
42        foreach ($this->events as $events) {
43            if (isset($events[$eventIri->value])) {
44                return self::snapshot($events[$eventIri->value]);
45            }
46        }
47
48        return null;
49    }
50
51    /**
52     * A caller editing the event it passed in or read back must not edit the log (R2-005).
53     */
54    private static function snapshot(LogisticsEvent $event): LogisticsEvent
55    {
56        return new LogisticsEvent($event->iri, $event->logisticsObject, new Graph($event->graph), $event->created);
57    }
58
59    public function query(Iri $logisticsObject, EventQuery $query): array
60    {
61        $events = array_values($this->events[$logisticsObject->value] ?? []);
62        $events = array_values(array_filter($events, static function (LogisticsEvent $event) use ($query): bool {
63            if ($query->eventCodes !== [] && array_filter($query->eventCodes, static fn(string $c): bool => $event->matchesCode($c)) === []) {
64                return false;
65            }
66            // eventDate is guaranteed by the server and the checked builder; the receipt-time fallbacks
67            // below only keep this store total over events stored by other means.
68            $created = $event->creationDate() ?? $event->created;
69            $occurred = $event->eventDate();
70            if ($query->createdAfter !== null && $created <= $query->createdAfter) {
71                return false;
72            }
73            if ($query->createdBefore !== null && $created >= $query->createdBefore) {
74                return false;
75            }
76            if ($query->occurredAfter !== null && ($occurred === null || $occurred <= $query->occurredAfter)) {
77                return false;
78            }
79            if ($query->occurredBefore !== null && ($occurred === null || $occurred >= $query->occurredBefore)) {
80                return false;
81            }
82
83            return true;
84        }));
85        usort($events, static function (LogisticsEvent $a, LogisticsEvent $b) use ($query): int {
86            $byEvent = str_ends_with($query->sort, 'eventDate');
87            $ka = $byEvent ? ($a->eventDate() ?? $a->created) : ($a->creationDate() ?? $a->created);
88            $kb = $byEvent ? ($b->eventDate() ?? $b->created) : ($b->creationDate() ?? $b->created);
89            $cmp = $ka <=> $kb;
90            if ($cmp === 0) {
91                $cmp = strcmp($a->iri->value, $b->iri->value);
92            }
93
94            return str_starts_with($query->sort, 'DESC') ? -$cmp : $cmp;
95        });
96
97        return array_map(self::snapshot(...), \array_slice($events, $query->skip, $query->limit));
98    }
99
100    public function lastModified(Iri $logisticsObject): ?DateTimeImmutable
101    {
102        return $this->lastModified[$logisticsObject->value] ?? null;
103    }
104}