Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
Psr6OptionsCache
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
6 / 6
11
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%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 set
100.00% covered (success)
100.00%
9 / 9
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\Cache\CacheException;
27use Psr\Cache\CacheItemPoolInterface;
28
29/**
30 * OPTIONS-cache adapter that delegates to any PSR-6 (Cache Item Pool) 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 pool returns is not trusted: a value that is not a whole, current entry for exactly this key is a miss, also
35 * when the pool still holds it past its end. The contract and the limits are those of {@see OptionsCacheInterface}; the
36 * number of items in a pool that many applications share is the pool's to bound. Entries of earlier versions are not
37 * read and not deleted.
38 *
39 * Every key the pool sees is 64 hex digits from {@see OptionsCacheKey::backend()}, whatever key the caller passes, so
40 * a host name, a service path or a prefix cannot break the PSR-6 rules for keys.
41 *
42 * A PSR-6 {@see CacheException} becomes an {@see IcapStorageException} with that exception as its cause; anything
43 * else the pool throws passes unchanged.
44 *
45 * Requires `psr/cache` ^3.0 — listed in composer.json `suggest`.
46 */
47final class Psr6OptionsCache implements OptionsCacheInterface
48{
49    /** @var Closure(): int */
50    private readonly Closure $clock;
51
52    /**
53     * @param string                $prefix separates adapters that share a pool, tenants for one; part of the hashed
54     *                                      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 CacheItemPoolInterface $pool,
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            $item = $this->pool->getItem($this->backendKey($key));
70            if (!$item->isHit()) {
71                return null;
72            }
73
74            $stored = $item->get();
75        } catch (CacheException $e) {
76            throw self::failed($e);
77        }
78
79        return OptionsEntry::fromStored($stored, $key, ($this->clock)())?->response;
80    }
81
82    #[\Override]
83    public function set(string $key, IcapResponse $response, int $ttlSeconds, ?string $istag = null): void
84    {
85        $entry = OptionsEntry::from($key, $response, $ttlSeconds, ($this->clock)());
86        if ($entry === null) {
87            return;
88        }
89
90        try {
91            $item = $this->pool->getItem($this->backendKey($key));
92            $item->set($entry->toArray());
93            $item->expiresAfter($entry->lifetime());
94            $this->pool->save($item);
95        } catch (CacheException $e) {
96            throw self::failed($e);
97        }
98    }
99
100    #[\Override]
101    public function delete(string $key): void
102    {
103        try {
104            $this->pool->deleteItem($this->backendKey($key));
105        } catch (CacheException $e) {
106            throw self::failed($e);
107        }
108    }
109
110    private static function failed(CacheException $cause): IcapStorageException
111    {
112        return new IcapStorageException('The OPTIONS cache pool failed.', 0, $cause);
113    }
114
115    private function backendKey(string $key): string
116    {
117        return OptionsCacheKey::backend($this->prefix, $key);
118    }
119}