Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.06% covered (success)
97.06%
33 / 34
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Fanout
97.06% covered (success)
97.06%
33 / 34
83.33% covered (warning)
83.33%
5 / 6
14
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 logisticsObjectCreated
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 logisticsObjectUpdated
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 logisticsEventReceived
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 actionRequestStatusChanged
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 toSubscribers
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace LambdaTwelve\OneRecord\Server\Notification;
6
7use LambdaTwelve\OneRecord\Api\ActionRequest;
8use LambdaTwelve\OneRecord\Api\Notification;
9use LambdaTwelve\OneRecord\Api\NotificationEventType;
10use LambdaTwelve\OneRecord\Api\SubscriptionEventType;
11use LambdaTwelve\OneRecord\Model\LogisticsEvent;
12use LambdaTwelve\OneRecord\Rdf\Iri;
13use LambdaTwelve\OneRecord\Server\Services;
14use LambdaTwelve\OneRecord\Server\Spi\Action;
15use LambdaTwelve\OneRecord\Server\Spi\Agent;
16use LambdaTwelve\OneRecord\Server\Spi\Decision;
17use LambdaTwelve\OneRecord\Server\Spi\OutboundNotification;
18use LambdaTwelve\OneRecord\Server\Spi\StoredObject;
19
20/**
21 * Decides who gets told about what, and enqueues the notifications. Subscribers
22 * of an object (by URI or by type) hear about creation, updates and events
23 * according to the event types they asked for; requesters who set
24 * notifyRequestStatusChange hear about their action requests.
25 */
26final class Fanout
27{
28    public function __construct(private readonly Services $services) {}
29
30    public function logisticsObjectCreated(StoredObject $stored, ?Iri $triggeredBy = null): void
31    {
32        $this->toSubscribers($stored, SubscriptionEventType::LogisticsObjectCreated, static fn(Notification $n): Notification => $n, $triggeredBy);
33    }
34
35    /**
36     * @param list<string> $changedProperties
37     */
38    public function logisticsObjectUpdated(StoredObject $stored, array $changedProperties, ?Iri $triggeredBy = null): void
39    {
40        $this->toSubscribers($stored, SubscriptionEventType::LogisticsObjectUpdated, static fn(Notification $n): Notification => new Notification($n->eventType, $n->logisticsObject, $n->logisticsObjectType, $n->triggeredBy, $changedProperties, [], $n->body), $triggeredBy);
41    }
42
43    public function logisticsEventReceived(StoredObject $stored, LogisticsEvent $event): void
44    {
45        $this->toSubscribers($stored, SubscriptionEventType::LogisticsEventReceived, static fn(Notification $n): Notification => new Notification($n->eventType, $n->logisticsObject, $n->logisticsObjectType, $n->triggeredBy, [], [$event->iri], $n->body));
46    }
47
48    /**
49     * Tells the requester about a status change when it asked to be told.
50     */
51    public function actionRequestStatusChanged(ActionRequest $request): void
52    {
53        if (!$request->notifyRequestStatusChange()) {
54            return;
55        }
56        // api:hasLogisticsObject is at most one; a delegation over several objects names none of them
57        // rather than an arbitrary first one, and the request in isTriggeredBy lists them all (spec question 32).
58        $objects = $request->logisticsObjects();
59        $object = \count($objects) === 1 ? $objects[0] : null;
60        $type = null;
61        if ($object !== null) {
62            $stored = $this->services->objects->latest($object);
63            $type = $stored?->object->mostSpecificType($this->services->vocabulary);
64        }
65        $notification = new Notification($request->type->notificationFor($request->status), $object, $type, $request->iri);
66        $this->services->outbox->enqueue(new OutboundNotification($request->requestedBy, $notification, $this->services->clock->now(), $this->services->ids->next()));
67    }
68
69    /**
70     * @param callable(Notification): Notification $decorate
71     */
72    private function toSubscribers(StoredObject $stored, SubscriptionEventType $eventType, callable $decorate, ?Iri $triggeredBy = null): void
73    {
74        $object = $stored->object;
75        $type = $object->mostSpecificType($this->services->vocabulary);
76        $now = $this->services->clock->now();
77        foreach ($this->services->subscriptions->subscribersOf($object->iri, $object->types(), $now) as $entry) {
78            $subscription = $entry['subscription'];
79            if (!$subscription->includes($eventType)) {
80                continue;
81            }
82            // A subscription says what to tell; the access policy says what may be disclosed (spec question 25).
83            // No read permission: the subscriber learns that something happened, not what. Hidden: nothing at all.
84            $decision = $this->services->policy->decide(new Agent($subscription->subscriber), Action::ReadLogisticsObject, $object->iri);
85            if ($decision === Decision::Hide) {
86                continue;
87            }
88            $base = new Notification(
89                NotificationEventType::from($eventType->value),
90                $object->iri,
91                $type,
92                $triggeredBy ?? $entry['request'],
93                [],
94                [],
95                $subscription->sendLogisticsObjectBody && $decision === Decision::Allow ? $object : null,
96            );
97            $this->services->outbox->enqueue(new OutboundNotification($subscription->subscriber, $decorate($base), $now, $this->services->ids->next()));
98        }
99    }
100}