Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
Psr16OptionsCache
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
6 / 6
10
100.00% covered (success)
100.00%
1 / 1
 __construct
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
2
 set
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 delete
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 failed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 backendKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * SPDX-License-Identifier: EUPL-1.2
5 *
6 * This file is part of icap-flow.
7 *
8 * Licensed under the EUPL, Version 1.2 only (the "Licence");
9 * you may not use this work except in compliance with the Licence.
10 * You may obtain a copy of the Licence at:
11 *
12 *     https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the Licence is distributed on an "AS IS" basis,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 */
18
19declare(strict_types=1);
20
21namespace Ndrstmr\Icap\Cache;
22
23use Closure;
24use Ndrstmr\Icap\DTO\IcapResponse;
25use Ndrstmr\Icap\Exception\IcapStorageException;
26use Psr\SimpleCache\CacheException;
27use Psr\SimpleCache\CacheInterface;
28
29/**
30 * OPTIONS-cache adapter that delegates to any PSR-16 (Simple Cache) implementation.
31 *
32 * One item per entry, in the form of {@see OptionsEntry}, with a lifetime of at most an hour; nothing else is stored, no
33 * ISTag item and no list of keys, so that writers in several processes cannot lose or invalidate each other's entries.
34 * What the backend returns is not trusted: a value that is not a whole, current entry for exactly this key is a miss,
35 * also when the backend still holds it past its end. The contract and the limits are those of
36 * {@see OptionsCacheInterface}; the number of items in a backend that many applications share is the backend's to
37 * bound. Entries of earlier versions are not read and not deleted.
38 *
39 * Every key the backend sees is 64 hex digits from {@see OptionsCacheKey::backend()}, whatever key the caller passes,
40 * so a host name, a service path or a prefix cannot break the PSR-16 rules for keys.
41 *
42 * A PSR-16 {@see CacheException} becomes an {@see IcapStorageException} with that exception as its cause; anything
43 * else the backend throws passes unchanged.
44 *
45 * Requires `psr/simple-cache` ^3.0 — listed in composer.json `suggest`.
46 */
47final class Psr16OptionsCache implements OptionsCacheInterface
48{
49    /** @var Closure(): int */
50    private readonly Closure $clock;
51
52    /**
53     * @param string                $prefix separates adapters that share a backend, tenants for one; part of the
54     *                                      hashed key, not put in front of it
55     * @param (Closure(): int)|null $clock  injectable clock for deterministic tests; defaults to time()
56     */
57    public function __construct(
58        private readonly CacheInterface $cache,
59        private readonly string $prefix = '',
60        ?Closure $clock = null,
61    ) {
62        $this->clock = $clock ?? static fn (): int => time();
63    }
64
65    #[\Override]
66    public function get(string $key): ?IcapResponse
67    {
68        try {
69            $stored = $this->cache->get($this->backendKey($key));
70        } catch (CacheException $e) {
71            throw self::failed($e);
72        }
73
74        return OptionsEntry::fromStored($stored, $key, ($this->clock)())?->response;
75    }
76
77    #[\Override]
78    public function set(string $key, IcapResponse $response, int $ttlSeconds, ?string $istag = null): void
79    {
80        $entry = OptionsEntry::from($key, $response, $ttlSeconds, ($this->clock)());
81        if ($entry === null) {
82            return;
83        }
84
85        try {
86            $this->cache->set($this->backendKey($key), $entry->toArray(), $entry->lifetime());
87        } catch (CacheException $e) {
88            throw self::failed($e);
89        }
90    }
91
92    #[\Override]
93    public function delete(string $key): void
94    {
95        try {
96            $this->cache->delete($this->backendKey($key));
97        } catch (CacheException $e) {
98            throw self::failed($e);
99        }
100    }
101
102    private static function failed(CacheException $cause): IcapStorageException
103    {
104        return new IcapStorageException('The OPTIONS cache backend failed.', 0, $cause);
105    }
106
107    private function backendKey(string $key): string
108    {
109        return OptionsCacheKey::backend($this->prefix, $key);
110    }
111}