Passed
Push — donut_body_cache ( d3062b...89e861 )
by Akihito
02:28
created

ResourceStorage::evaluateDonutBody()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 4
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 9
rs 10
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
    use ResourceStorageCacheableTrait;
34
35
    /**
36
     * Resource object cache prefix
37
     */
38
    private const KEY_RO = 'ro-';
39
40
    /**
41
     * Resource static cache prifix
42
     */
43
    private const KEY_DONUT = 'donut-';
44
45
    /** @var RepositoryLoggerInterface */
46
    private $logger;
47
48
    /** @var TagAwareAdapter */
49
    private $roPool;
50
51
    /** @var TagAwareAdapter */
52
    private $etagPool;
53
54
    /** @var PurgerInterface */
55
    private $purger;
56
57
    /** @var UriTagInterface */
58
    private $uriTag;
59
60
    /** @var ResourceStorageSaver */
61
    private $saver;
62
63
    /** @var float */
64
    private $knownTagTtl;
65
66
    /** @var bool */
67
    private $isOptimizeCache;
68
69
    /**
70
     * @Shared("pool")
71
     * @EtagPool("etagPool")
72
     * @KnownTagTtl("knownTagTtl")
73
     * @IsOptimizeCache("isOptimizeCache")
74
     */
75
    #[Shared('pool'), EtagPool('etagPool'), KnownTagTtl('knownTagTtl'), IsOptimizeCache('isOptimizeCache')]
76
    public function __construct(
77
        RepositoryLoggerInterface $logger,
78
        PurgerInterface $etagDeleter,
79
        UriTagInterface $uriTag,
80
        ?CacheItemPoolInterface $pool = null,
81
        ?CacheItemPoolInterface $etagPool = null,
82
        ?CacheProvider $cache = null,
83
        float $knownTagTtl = 0.0,
84
        bool $isOptimizeCache = false
85
    ) {
86
        $this->logger = $logger;
87
        $this->purger = $etagDeleter;
88
        $this->uriTag = $uriTag;
89
        $this->saver = new ResourceStorageSaver();
90
        if ($pool === null && $cache instanceof CacheProvider) {
91
            $this->injectDoctrineCache($cache);
92
93
            return;
94
        }
95
96
        $this->knownTagTtl = $knownTagTtl;
97
        assert($pool instanceof AdapterInterface);
98
        $etagPool =  $etagPool instanceof AdapterInterface ? $etagPool : $pool;
99
        $this->roPool = new TagAwareAdapter($pool, $etagPool, $knownTagTtl);
100
        $this->etagPool = new TagAwareAdapter($etagPool, $etagPool, $knownTagTtl);
101
        $this->isOptimizeCache = $isOptimizeCache;
102
    }
103
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
    }
110
111
    /**
112
     * {@inheritdoc}
113
     */
114
    public function get(AbstractUri $uri): ?ResourceState
115
    {
116
        $item = $this->roPool->getItem($this->getUriKey($uri, self::KEY_RO));
117
        assert($item instanceof ItemInterface);
118
        $state = $item->get();
119
        assert($state instanceof ResourceState || $state === null);
120
121
        return $state;
122
    }
123
124
    public function getDonut(AbstractUri $uri): ?ResourceDonut
125
    {
126
        $key = $this->getUriKey($uri, self::KEY_DONUT);
127
        $item = $this->roPool->getItem($key);
128
        assert($item instanceof ItemInterface);
129
        $donut = $item->get();
130
        assert($donut instanceof ResourceDonut || $donut === null);
131
132
        return $donut;
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138
    public function hasEtag(string $etag): bool
139
    {
140
        return $this->etagPool->hasItem($etag);
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146
    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
     * @return array<mixed>
222
     */
223
    private function evaluateDonutBody(array $body): array
224
    {
225
        foreach ($body as $key => $item) {
226
            if ($item instanceof DonutRequest) {
227
                $body[$key] = $item->getBody();
228
            }
229
        }
230
231
        return $body;
232
    }
233
234
    /**
235
     * @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...
236
     */
237
    private function getTags(ResourceObject $ro): array
238
    {
239
        $etag = $ro->headers['ETag'];
240
        $tags = [$etag, ($this->uriTag)($ro->uri)];
241
        if (isset($ro->headers[Header::SURROGATE_KEY])) {
242
            $tags = array_merge($tags, explode(' ', $ro->headers[Header::SURROGATE_KEY]));
243
        }
244
245
        /** @var list<string> $uniqueTags */
246
        $uniqueTags = array_unique($tags);
247
248
        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...
249
    }
250
251
    /**
252
     * @param mixed $body
253
     *
254
     * @return mixed
255
     */
256
    private function evaluateBody($body)
257
    {
258
        if (! is_array($body)) {
259
            return $body;
260
        }
261
262
        /** @psalm-suppress MixedAssignment $item */
263
        foreach ($body as &$item) {
264
            if ($item instanceof RequestInterface) {
265
                $item = ($item)();
266
            }
267
268
            if ($item instanceof ResourceObject) {
269
                $item->body = $this->evaluateBody($item->body);
270
            }
271
        }
272
273
        return $body;
274
    }
275
276
    private function getUriKey(AbstractUri $uri, string $type): string
277
    {
278
        return $type . ($this->uriTag)($uri) . (isset($_SERVER['X_VARY']) ? $this->getVary() : '');
279
    }
280
281
    private function getVary(): string
282
    {
283
        $xvary = $_SERVER['X_VARY'];
284
        assert(is_string($xvary));
285
        $varys = explode(',', $xvary);
286
        $varyString = '';
287
        foreach ($varys as $vary) {
288
            $phpVaryKey = sprintf('X_%s', strtoupper($vary));
289
            if (isset($_SERVER[$phpVaryKey]) && is_string($_SERVER[$phpVaryKey])) {
290
                $varyString .= $_SERVER[$phpVaryKey];
291
            }
292
        }
293
294
        return $varyString;
295
    }
296
297
    public function saveEtag(AbstractUri $uri, string $etag, string $surrogateKeys, ?int $ttl): void
298
    {
299
        $tags = $surrogateKeys !== '' ? explode(' ', $surrogateKeys) : [];
300
        $tags[] = (new UriTag())($uri);
301
        /** @var list<string> $uniqueTags */
302
        $uniqueTags = array_unique($tags);
303
        $this->logger->log('save-etag uri:%s etag:%s surrogate-keys:%s', $uri, $etag, $uniqueTags);
304
        $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

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