Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.33% covered (warning)
83.33%
15 / 18
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
Services
83.33% covered (warning)
83.33%
15 / 18
0.00% covered (danger)
0.00%
0 / 2
7.23
0.00% covered (danger)
0.00%
0 / 1
 __construct
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
2.01
 checks
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
5.27
1<?php
2
3declare(strict_types=1);
4
5namespace LambdaTwelve\OneRecord\Server;
6
7use LambdaTwelve\OneRecord\Model\EmbeddedIdMinter;
8use LambdaTwelve\OneRecord\Model\Uuid5EmbeddedIdMinter;
9use LambdaTwelve\OneRecord\Server\Http\RequestBody;
10use LambdaTwelve\OneRecord\Server\Http\Responder;
11use LambdaTwelve\OneRecord\Server\Spi\AccessDelegationStore;
12use LambdaTwelve\OneRecord\Server\Spi\AccessPolicy;
13use LambdaTwelve\OneRecord\Server\Spi\ActionRequestStore;
14use LambdaTwelve\OneRecord\Server\Spi\Authenticator;
15use LambdaTwelve\OneRecord\Server\Spi\IdGenerator;
16use LambdaTwelve\OneRecord\Server\Spi\LogisticsEventStore;
17use LambdaTwelve\OneRecord\Server\Spi\LogisticsObjectStore;
18use LambdaTwelve\OneRecord\Server\Spi\NotificationOutbox;
19use LambdaTwelve\OneRecord\Server\Spi\SubscriptionStore;
20use LambdaTwelve\OneRecord\Server\Spi\UnitOfWork;
21use LambdaTwelve\OneRecord\Server\Spi\Volatile;
22use LambdaTwelve\OneRecord\Vocabulary\Vocabulary;
23use Psr\Clock\ClockInterface;
24use Psr\EventDispatcher\EventDispatcherInterface;
25use Psr\Http\Message\ResponseFactoryInterface;
26use Psr\Http\Message\StreamFactoryInterface;
27use Psr\Log\LoggerInterface;
28use Psr\Log\NullLogger;
29
30/**
31 * Everything the endpoints share: the host's SPI implementations and PSR
32 * services, plus the helpers built from them. One object, so an endpoint's
33 * constructor stays a single parameter and a host wires things in one place.
34 */
35final readonly class Services
36{
37    public Responder $responder;
38    public RequestBody $body;
39    public Vocabulary $vocabulary;
40    public EmbeddedIdMinter $embeddedIds;
41    public LoggerInterface $logger;
42    public IdGenerator $ids;
43    public UnitOfWork $unitOfWork;
44
45    public function __construct(
46        public ServerConfig $config,
47        public LogisticsObjectStore $objects,
48        public LogisticsEventStore $events,
49        public ActionRequestStore $actionRequests,
50        public SubscriptionStore $subscriptions,
51        public AccessDelegationStore $delegations,
52        public NotificationOutbox $outbox,
53        public Authenticator $authenticator,
54        public AccessPolicy $policy,
55        public ClockInterface $clock,
56        public EventDispatcherInterface $dispatcher,
57        ResponseFactoryInterface $responses,
58        StreamFactoryInterface $streams,
59        ?LoggerInterface $logger = null,
60        ?Vocabulary $vocabulary = null,
61        ?EmbeddedIdMinter $embeddedIds = null,
62        ?IdGenerator $ids = null,
63        ?UnitOfWork $unitOfWork = null,
64    ) {
65        $this->responder = new Responder($responses, $streams);
66        $this->body = new RequestBody($config->maxBodyBytes);
67        // The newest configured data model is the validation ceiling, as ServerConfig says (D7-001).
68        $this->vocabulary = $vocabulary ?? Vocabulary::for($config->validationModel());
69        $this->embeddedIds = $embeddedIds ?? new Uuid5EmbeddedIdMinter();
70        $this->logger = $logger ?? new NullLogger();
71        $this->ids = $ids ?? new UuidIdGenerator();
72        $this->unitOfWork = $unitOfWork ?? new IdentityUnitOfWork();
73        foreach ($this->checks() as $finding) {
74            $this->logger->warning($finding);
75        }
76    }
77
78    /**
79     * What an operator should know about this wiring, in plain sentences; empty
80     * when nothing is amiss. Logged at construction and offered to the host's
81     * status page through ServerBuilder::check().
82     *
83     * @return list<string>
84     */
85    public function checks(): array
86    {
87        $findings = [];
88        if ($this->unitOfWork instanceof IdentityUnitOfWork) {
89            // Without a transaction a failed operation leaves what it had already written. Only
90            // stores marked Volatile have nothing to roll back; a host that persists must bind its own.
91            $persistent = [];
92            foreach (['objects' => $this->objects, 'events' => $this->events, 'actionRequests' => $this->actionRequests, 'subscriptions' => $this->subscriptions, 'delegations' => $this->delegations, 'outbox' => $this->outbox] as $name => $store) {
93                if (!$store instanceof Volatile) {
94                    $persistent[] = $name . ' (' . $store::class . ')';
95                }
96            }
97            if ($persistent !== []) {
98                $findings[] = \sprintf('No UnitOfWork is bound and these stores are not marked Volatile: %s. Operations will not be atomic; bind your database transaction as the UnitOfWork (a decorator around an in-memory store should implement Spi\\Volatile).', implode(', ', $persistent));
99            }
100        }
101
102        return $findings;
103    }
104}