Passed
Pull Request — 1.x (#113)
by Akihito
04:06 queued 01:49
created

ResourceStorage   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 276
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 20
Bugs 1 Features 3
Metric Value
eloc 101
c 20
b 1
f 3
dl 0
loc 276
ccs 52
cts 52
cp 1
rs 9.36
wmc 38

17 Methods

Rating   Name   Duplication   Size   Complexity  
A invalidateTags() 0 9 3
A injectDoctrineCache() 0 5 1
A saveDonut() 0 6 1
A hasEtag() 0 3 1
A deleteEtag() 0 5 1
A saveValue() 0 10 1
A __construct() 0 27 4
A getDonut() 0 9 2
A get() 0 8 2
A saveView() 0 10 1
A saveDonutView() 0 9 3
A evaluateDonutBody() 0 9 3
A evaluateBody() 0 18 5
A saveEtag() 0 8 2
A getTags() 0 12 2
A getVary() 0 14 4
A getUriKey() 0 3 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BEAR\QueryRepository;
6
7
use BEAR\QueryRepository\Annotation\IsOptimizeCache;
8
use BEAR\RepositoryModule\Annotation\EtagPool;
9
use BEAR\RepositoryModule\Annotation\KnownTagTtl;
10
use BEAR\Resource\AbstractUri;
11
use BEAR\Resource\RequestInterface;
12
use BEAR\Resource\ResourceObject;
13
use Doctrine\Common\Cache\CacheProvider;
14
use Psr\Cache\CacheItemPoolInterface;
15
use Ray\PsrCacheModule\Annotation\Shared;
16
use Symfony\Component\Cache\Adapter\AdapterInterface;
17
use Symfony\Component\Cache\Adapter\DoctrineAdapter;
18
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
19
use Symfony\Contracts\Cache\ItemInterface;
20
21
use function array_merge;
22
use function array_unique;
23
use function assert;
24
use function explode;
25
use function implode;
26
use function is_array;
27
use function is_string;
28
use function sprintf;
29
use function strtoupper;
30
31
final class ResourceStorage implements ResourceStorageInterface
32
{
33 30
    use ResourceStorageCacheableTrait;
34
35 30
    /**
36 30
     * Resource object cache prefix
37
     */
38
    private const KEY_RO = 'ro-';
39
40
    /**
41 3
     * Resource static cache prifix
42
     */
43 3
    private const KEY_DONUT = 'donut-';
44
45
    /** @var RepositoryLoggerInterface */
46
    private $logger;
47
48
    /** @var TagAwareAdapter */
49 26
    private $roPool;
50
51 26
    /** @var TagAwareAdapter */
52 26
    private $etagPool;
53 26
54
    /** @var PurgerInterface */
55 26
    private $purger;
56
57 26
    /** @var UriTagInterface */
58
    private $uriTag;
59 26
60 26
    /** @var ResourceStorageSaver */
61
    private $saver;
62
63
    /** @var float */
64
    private $knownTagTtl;
65 27
66
    /** @var bool */
67 27
    private $isOptimizeCache;
68 27
69
    /**
70 27
     * @Shared("pool")
71 27
     * @EtagPool("etagPool")
72
     * @KnownTagTtl("knownTagTtl")
73
     * @IsOptimizeCache("isOptimizeCache")
74
     */
75
    #[Shared('pool'), EtagPool('etagPool'), KnownTagTtl('knownTagTtl'), IsOptimizeCache('isOptimizeCache')]
76 25
    public function __construct(
77
        RepositoryLoggerInterface $logger,
78 25
        PurgerInterface $etagDeleter,
79
        UriTagInterface $uriTag,
80 25
        ?CacheItemPoolInterface $pool = null,
81
        ?CacheItemPoolInterface $etagPool = null,
82
        ?CacheProvider $cache = null,
83
        float $knownTagTtl = 0.0,
84
        bool $isOptimizeCache = false
85
    ) {
86 12
        $this->logger = $logger;
87
        $this->purger = $etagDeleter;
88 12
        $this->uriTag = $uriTag;
89 12
        $this->saver = new ResourceStorageSaver();
90
        if ($pool === null && $cache instanceof CacheProvider) {
91 12
            $this->injectDoctrineCache($cache);
92
93
            return;
94
        }
95
96
        $this->knownTagTtl = $knownTagTtl;
97 24
        assert($pool instanceof AdapterInterface);
98
        $etagPool =  $etagPool instanceof AdapterInterface ? $etagPool : $pool;
99 24
        $this->roPool = new TagAwareAdapter($pool, $etagPool, $knownTagTtl);
100 24
        $this->etagPool = new TagAwareAdapter($etagPool, $etagPool, $knownTagTtl);
101 24
        $this->isOptimizeCache = $isOptimizeCache;
102
    }
103 24
104
    private function injectDoctrineCache(CacheProvider $cache): void
105
    {
106
        /** @psalm-suppress DeprecatedClass */
107
        $this->roPool = new TagAwareAdapter(new DoctrineAdapter($cache));
0 ignored issues
show
Deprecated Code introduced by
The class Symfony\Component\Cache\Adapter\DoctrineAdapter has been deprecated: Since Symfony 5.4, use Doctrine\Common\Cache\Psr6\CacheAdapter instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

107
        $this->roPool = new TagAwareAdapter(/** @scrutinizer ignore-deprecated */ new DoctrineAdapter($cache));
Loading history...
108
        $this->etagPool = $this->roPool;
109 2
    }
110
111 2
    /**
112 2
     * {@inheritdoc}
113 2
     */
114
    public function get(AbstractUri $uri): ?ResourceState
115 2
    {
116
        $item = $this->roPool->getItem($this->getUriKey($uri, self::KEY_RO));
117
        assert($item instanceof ItemInterface);
118 26
        $state = $item->get();
119
        assert($state instanceof ResourceState || $state === null);
120 26
121 8
        return $state;
122
    }
123 18
124 18
    public function getDonut(AbstractUri $uri): ?ResourceDonut
125 2
    {
126
        $key = $this->getUriKey($uri, self::KEY_DONUT);
127
        $item = $this->roPool->getItem($key);
128
        assert($item instanceof ItemInterface);
129 18
        $donut = $item->get();
130
        assert($donut instanceof ResourceDonut || $donut === null);
131
132 27
        return $donut;
133
    }
134 27
135 18
    /**
136
     * {@inheritdoc}
137 10
     */
138 10
    public function hasEtag(string $etag): bool
139 10
    {
140 10
        return $this->etagPool->hasItem($etag);
141 10
    }
142 10
143
    /**
144
     * {@inheritdoc}
145
     */
146 10
    public function deleteEtag(AbstractUri $uri)
147
    {
148
        $uriTag = ($this->uriTag)($uri);
149
150
        return $this->invalidateTags([$uriTag]);
151
    }
152
153
    /**
154
     * {@inheritdoc}
155
     */
156
    public function invalidateTags(array $tags): bool
157
    {
158
        $tag = $tags !== [] ? implode(' ', $tags) : '';
159
        $this->logger->log('invalidate-etag tags:%s', $tag);
160
        $valid1 = $this->roPool->invalidateTags($tags);
161
        $valid2 = $this->etagPool->invalidateTags($tags);
162
        ($this->purger)(implode(' ', $tags));
163
164
        return $valid1 && $valid2;
165
    }
166
167
    /**
168
     * {@inheritdoc}
169
     *
170
     * @return bool
171
     */
172
    public function saveValue(ResourceObject $ro, int $ttl)
173
    {
174
        /** @psalm-suppress MixedAssignment $body */
175
        $body = $this->evaluateBody($ro->body);
176
        $value = ResourceState::create($ro, $body, null);
177
        $key = $this->getUriKey($ro->uri, self::KEY_RO);
178
        $tags = $this->getTags($ro);
179
        $this->logger->log('save-value uri:%s tags:%s ttl:%s', $ro->uri, $tags, $ttl);
180
181
        return $this->saver->__invoke($key, $value, $this->roPool, $tags, $ttl);
182
    }
183
184
    /**
185
     * {@inheritdoc}
186
     *
187
     * @return bool
188
     */
189
    public function saveView(ResourceObject $ro, int $ttl)
190
    {
191
        $this->logger->log('save-view uri:%s ttl:%s', $ro->uri, $ttl);
192
        /** @psalm-suppress MixedAssignment $body */
193
        $body = $this->evaluateBody($ro->body);
194
        $value = ResourceState::create($ro, $body, $ro->view);
195
        $key = $this->getUriKey($ro->uri, self::KEY_RO);
196
        $tags = $this->getTags($ro);
197
198
        return $this->saver->__invoke($key, $value, $this->roPool, $tags, $ttl);
199
    }
200
201
    public function saveDonut(AbstractUri $uri, ResourceDonut $donut, ?int $sMaxAge): void
202
    {
203
        $key = $this->getUriKey($uri, self::KEY_DONUT);
204
        $this->logger->log('save-donut uri:%s s-maxage:%s', $uri, $sMaxAge);
205
206
        $this->saver->__invoke($key, $donut, $this->roPool, [], $sMaxAge);
207
    }
208
209
    public function saveDonutView(ResourceObject $ro, ?int $ttl): bool
210
    {
211
        $body = $this->isOptimizeCache || ! is_array($ro->body) ? [] : $this->evaluateDonutBody($ro->body);
212
        $resourceState = ResourceState::create($ro, $body, $ro->view);
213
        $key = $this->getUriKey($ro->uri, self::KEY_RO);
214
        $tags = $this->getTags($ro);
215
        $this->logger->log('save-donut-view uri:%s surrogate-keys:%s s-maxage:%s', $ro->uri, $tags, $ttl);
216
217
        return $this->saver->__invoke($key, $resourceState, $this->roPool, $tags, $ttl);
218
    }
219
220
    /**
221
     * @param array<mixed> $body
222
     *
223
     * @return array<mixed>
224
     */
225
    private function evaluateDonutBody(array $body): array
226
    {
227
        foreach ($body as $key => $item) {
228
            if ($item instanceof DonutRequest) {
229
                $body[$key] = $item->getBody();
230
            }
231
        }
232
233
        return $body;
234
    }
235
236
    /**
237
     * @return list<string>
0 ignored issues
show
Bug introduced by
The type BEAR\QueryRepository\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
238
     */
239
    private function getTags(ResourceObject $ro): array
240
    {
241
        $etag = $ro->headers['ETag'];
242
        $tags = [$etag, ($this->uriTag)($ro->uri)];
243
        if (isset($ro->headers[Header::SURROGATE_KEY])) {
244
            $tags = array_merge($tags, explode(' ', $ro->headers[Header::SURROGATE_KEY]));
245
        }
246
247
        /** @var list<string> $uniqueTags */
248
        $uniqueTags = array_unique($tags);
249
250
        return $uniqueTags;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $uniqueTags returns the type BEAR\QueryRepository\list which is incompatible with the type-hinted return array.
Loading history...
251
    }
252
253
    /**
254
     * @param mixed $body
255
     *
256
     * @return mixed
257
     */
258
    private function evaluateBody($body)
259
    {
260
        if (! is_array($body)) {
261
            return $body;
262
        }
263
264
        /** @psalm-suppress MixedAssignment $item */
265
        foreach ($body as &$item) {
266
            if ($item instanceof RequestInterface) {
267
                $item = ($item)();
268
            }
269
270
            if ($item instanceof ResourceObject) {
271
                $item->body = $this->evaluateBody($item->body);
272
            }
273
        }
274
275
        return $body;
276
    }
277
278
    private function getUriKey(AbstractUri $uri, string $type): string
279
    {
280
        return $type . ($this->uriTag)($uri) . (isset($_SERVER['X_VARY']) ? $this->getVary() : '');
281
    }
282
283
    private function getVary(): string
284
    {
285
        $xvary = $_SERVER['X_VARY'];
286
        assert(is_string($xvary));
287
        $varys = explode(',', $xvary);
288
        $varyString = '';
289
        foreach ($varys as $vary) {
290
            $phpVaryKey = sprintf('X_%s', strtoupper($vary));
291
            if (isset($_SERVER[$phpVaryKey]) && is_string($_SERVER[$phpVaryKey])) {
292
                $varyString .= $_SERVER[$phpVaryKey];
293
            }
294
        }
295
296
        return $varyString;
297
    }
298
299
    public function saveEtag(AbstractUri $uri, string $etag, string $surrogateKeys, ?int $ttl): void
300
    {
301
        $tags = $surrogateKeys !== '' ? explode(' ', $surrogateKeys) : [];
302
        $tags[] = (new UriTag())($uri);
303
        /** @var list<string> $uniqueTags */
304
        $uniqueTags = array_unique($tags);
305
        $this->logger->log('save-etag uri:%s etag:%s surrogate-keys:%s', $uri, $etag, $uniqueTags);
306
        $this->saver->__invoke($etag, 'etag', $this->etagPool, $uniqueTags, $ttl);
0 ignored issues
show
Bug introduced by
$uniqueTags of type BEAR\QueryRepository\list is incompatible with the type array expected by parameter $tags of BEAR\QueryRepository\Res...torageSaver::__invoke(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

306
        $this->saver->__invoke($etag, 'etag', $this->etagPool, /** @scrutinizer ignore-type */ $uniqueTags, $ttl);
Loading history...
307
    }
308
}
309