Passed
Push — spike ( 62103d...a64745 )
by Akihito
02:45
created

ResourceStorage::evaluateBody()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 5
eloc 8
c 1
b 0
f 1
nc 6
nop 1
dl 0
loc 18
rs 9.6111
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BEAR\QueryRepository;
6
7
use BEAR\RepositoryModule\Annotation\EtagPool;
8
use BEAR\RepositoryModule\Annotation\KnownTagTtl;
9
use BEAR\Resource\AbstractUri;
10
use BEAR\Resource\RequestInterface;
11
use BEAR\Resource\ResourceObject;
12
use Doctrine\Common\Cache\CacheProvider;
13
use Psr\Cache\CacheItemPoolInterface;
14
use Ray\PsrCacheModule\Annotation\Shared;
15
use Symfony\Component\Cache\Adapter\AdapterInterface;
16
use Symfony\Component\Cache\Adapter\DoctrineAdapter;
17
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
18
use Symfony\Contracts\Cache\ItemInterface;
19
20
use function array_merge;
21
use function array_unique;
22
use function assert;
23
use function explode;
24
use function implode;
25
use function is_array;
26
use function is_string;
27
use function sprintf;
28
use function strtoupper;
29
30
final class ResourceStorage implements ResourceStorageInterface
31
{
32
    use ResourceStorageCacheableTrait;
33
34
    /**
35
     * Resource object cache prefix
36
     */
37
    private const KEY_RO = 'ro-';
38
39
    /**
40
     * Resource static cache prifix
41
     */
42
    private const KEY_DONUT = 'donut-';
43
44
    /** @var RepositoryLoggerInterface */
45
    private $logger;
46
47
    /** @var TagAwareAdapter */
48
    private $roPool;
49
50
    /** @var TagAwareAdapter */
51
    private $etagPool;
52
53
    /** @var PurgerInterface */
54
    private $purger;
55
56
    /** @var UriTagInterface */
57
    private $uriTag;
58
59
    /** @var ResourceStorageSaver */
60
    private $saver;
61
62
    /** @var float */
63
    private $knownTagTtl;
64
65
    /**
66
     * @Shared("pool")
67
     * @EtagPool("etagPool")
68
     * @KnownTagTtl("knownTagTtl")
69
     */
70
    #[Shared('pool'), EtagPool('etagPool'), KnownTagTtl('knownTagTtl')]
71
    public function __construct(
72
        RepositoryLoggerInterface $logger,
73
        PurgerInterface $etagDeleter,
74
        UriTagInterface $uriTag,
75
        ?CacheItemPoolInterface $pool = null,
76
        ?CacheItemPoolInterface $etagPool = null,
77
        ?CacheProvider $cache = null,
78
        float $knownTagTtl = 0.0
79
    ) {
80
        $this->logger = $logger;
81
        $this->purger = $etagDeleter;
82
        $this->uriTag = $uriTag;
83
        $this->saver = new ResourceStorageSaver();
84
        if ($pool === null && $cache instanceof CacheProvider) {
85
            $this->injectDoctrineCache($cache);
86
87
            return;
88
        }
89
90
        $this->knownTagTtl = $knownTagTtl;
91
        assert($pool instanceof AdapterInterface);
92
        $etagPool =  $etagPool instanceof AdapterInterface ? $etagPool : $pool;
93
        $this->roPool = new TagAwareAdapter($pool, $etagPool, $knownTagTtl);
94
        $this->etagPool = new TagAwareAdapter($etagPool, $etagPool, $knownTagTtl);
95
    }
96
97
    private function injectDoctrineCache(CacheProvider $cache): void
98
    {
99
        /** @psalm-suppress DeprecatedClass */
100
        $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

100
        $this->roPool = new TagAwareAdapter(/** @scrutinizer ignore-deprecated */ new DoctrineAdapter($cache));
Loading history...
101
        $this->etagPool = $this->roPool;
102
    }
103
104
    /**
105
     * {@inheritdoc}
106
     */
107
    public function get(AbstractUri $uri): ?ResourceState
108
    {
109
        $item = $this->roPool->getItem($this->getUriKey($uri, self::KEY_RO));
110
        assert($item instanceof ItemInterface);
111
        $state = $item->get();
112
        assert($state instanceof ResourceState || $state === null);
113
114
        return $state;
115
    }
116
117
    public function getDonut(AbstractUri $uri): ?ResourceDonut
118
    {
119
        $key = $this->getUriKey($uri, self::KEY_DONUT);
120
        $item = $this->roPool->getItem($key);
121
        assert($item instanceof ItemInterface);
122
        $donut = $item->get();
123
        assert($donut instanceof ResourceDonut || $donut === null);
124
125
        return $donut;
126
    }
127
128
    public function deleteDonut(AbstractUri $uri): void
129
    {
130
        $key = $this->getUriKey($uri, self::KEY_DONUT);
131
        $item = $this->roPool->deleteItem($key);
0 ignored issues
show
Unused Code introduced by
The assignment to $item is dead and can be removed.
Loading history...
132
        $this->deleteEtag($uri);
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
        $resourceState = ResourceState::create($ro, [], $ro->view);
212
        $key = $this->getUriKey($ro->uri, self::KEY_RO);
213
        $tags = $this->getTags($ro);
214
        $this->logger->log('save-donut-view uri:%s surrogate-keys:%s s-maxage:%s', $ro->uri, $tags, $ttl);
215
216
        return $this->saver->__invoke($key, $resourceState, $this->roPool, $tags, $ttl);
217
    }
218
219
    /**
220
     * @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...
221
     */
222
    private function getTags(ResourceObject $ro): array
223
    {
224
        $etag = $ro->headers['ETag'];
225
        $tags = [$etag, ($this->uriTag)($ro->uri)];
226
        if (isset($ro->headers[Header::SURROGATE_KEY])) {
227
            $tags = array_merge($tags, explode(' ', $ro->headers[Header::SURROGATE_KEY]));
228
        }
229
230
        /** @var list<string> $uniqueTags */
231
        $uniqueTags = array_unique($tags);
232
233
        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...
234
    }
235
236
    /**
237
     * @param mixed $body
238
     *
239
     * @return mixed
240
     */
241
    private function evaluateBody($body)
242
    {
243
        if (! is_array($body)) {
244
            return $body;
245
        }
246
247
        /** @psalm-suppress MixedAssignment $item */
248
        foreach ($body as &$item) {
249
            if ($item instanceof RequestInterface) {
250
                $item = ($item)();
251
            }
252
253
            if ($item instanceof ResourceObject) {
254
                $item->body = $this->evaluateBody($item->body);
255
            }
256
        }
257
258
        return $body;
259
    }
260
261
    private function getUriKey(AbstractUri $uri, string $type): string
262
    {
263
        return $type . ($this->uriTag)($uri) . (isset($_SERVER['X_VARY']) ? $this->getVary() : '');
264
    }
265
266
    private function getVary(): string
267
    {
268
        $xvary = $_SERVER['X_VARY'];
269
        assert(is_string($xvary));
270
        $varys = explode(',', $xvary);
271
        $varyString = '';
272
        foreach ($varys as $vary) {
273
            $phpVaryKey = sprintf('X_%s', strtoupper($vary));
274
            if (isset($_SERVER[$phpVaryKey]) && is_string($_SERVER[$phpVaryKey])) {
275
                $varyString .= $_SERVER[$phpVaryKey];
276
            }
277
        }
278
279
        return $varyString;
280
    }
281
282
    public function saveEtag(AbstractUri $uri, string $etag, string $surrogateKeys, ?int $ttl): void
283
    {
284
        $tags = $surrogateKeys !== '' ? explode(' ', $surrogateKeys) : [];
285
        $tags[] = (new UriTag())($uri);
286
        /** @var list<string> $uniqueTags */
287
        $uniqueTags = array_unique($tags);
288
        $this->logger->log('save-etag uri:%s etag:%s surrogate-keys:%s', $uri, $etag, $uniqueTags);
289
        $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

289
        $this->saver->__invoke($etag, 'etag', $this->etagPool, /** @scrutinizer ignore-type */ $uniqueTags, $ttl);
Loading history...
290
    }
291
}
292