Passed
Pull Request — 1.x (#100)
by Akihito
02:15
created

ResourceStorage::__unserialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 10
nc 1
nop 1
dl 0
loc 12
ccs 0
cts 0
cp 0
crap 2
rs 9.9332
c 0
b 0
f 0
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\Di\Di\Inject;
15
use Ray\Di\InjectorInterface;
16
use Ray\PsrCacheModule\Annotation\Shared;
17
use Serializable;
18
use Symfony\Component\Cache\Adapter\AdapterInterface;
19
use Symfony\Component\Cache\Adapter\DoctrineAdapter;
20
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
21
use Symfony\Contracts\Cache\ItemInterface;
22
23
use function array_merge;
24
use function array_unique;
25
use function assert;
26
use function explode;
27
use function implode;
28
use function is_array;
29
use function is_string;
30
use function sprintf;
31
use function strtoupper;
32
33 30
final class ResourceStorage implements ResourceStorageInterface, Serializable
34
{
35 30
    use Php73BcSerializableTrait;
36 30
37
    /**
38
     * Resource object cache prefix
39
     */
40
    private const KEY_RO = 'ro-';
41 3
42
    /**
43 3
     * Resource static cache prifix
44
     */
45
    private const KEY_DONUT = 'donut-';
46
47
    /** @var RepositoryLoggerInterface */
48
    private $logger;
49 26
50
    /** @var TagAwareAdapter */
51 26
    private $roPool;
52 26
53 26
    /** @var TagAwareAdapter */
54
    private $etagPool;
55 26
56
    /** @var PurgerInterface */
57 26
    private $purger;
58
59 26
    /** @var UriTagInterface */
60 26
    private $uriTag;
61
62
    /** @var ResourceStorageSaver */
63
    private $saver;
64
65 27
    /** @var float */
66
    private $knownTagTtl;
67 27
68 27
    /**
69
     * @var InjectorInterface
70 27
     * @psalm-suppress PropertyNotSetInConstructor
71 27
     */
72
    protected $injector;
73
74
    /**
75
     * @Shared("pool")
76 25
     * @EtagPool("etagPool")
77
     * @KnownTagTtl("knownTagTtl")
78 25
     */
79
    #[Shared('pool'), EtagPool('etagPool'), KnownTagTtl('knownTagTtl')]
80 25
    public function __construct(
81
        RepositoryLoggerInterface $logger,
82
        PurgerInterface $etagDeleter,
83
        UriTagInterface $uriTag,
84
        ?CacheItemPoolInterface $pool = null,
85
        ?CacheItemPoolInterface $etagPool = null,
86 12
        ?CacheProvider $cache = null,
87
        float $knownTagTtl = 0.0
88 12
    ) {
89 12
        $this->logger = $logger;
90
        $this->purger = $etagDeleter;
91 12
        $this->uriTag = $uriTag;
92
        $this->saver = new ResourceStorageSaver();
93
        if ($pool === null && $cache instanceof CacheProvider) {
94
            $this->injectDoctrineCache($cache);
95
96
            return;
97 24
        }
98
99 24
        $this->knownTagTtl = $knownTagTtl;
100 24
        assert($pool instanceof AdapterInterface);
101 24
        $etagPool =  $etagPool instanceof AdapterInterface ? $etagPool : $pool;
102
        $this->roPool = new TagAwareAdapter($pool, $etagPool, $knownTagTtl);
103 24
        $this->etagPool = new TagAwareAdapter($etagPool, $etagPool, $knownTagTtl);
104
    }
105
106
    /**
107
     * @Inject
108
     */
109 2
    #[Inject]
110
    public function setInjector(InjectorInterface $injector): void
111 2
    {
112 2
        $this->injector = $injector;
113 2
    }
114
115 2
    private function injectDoctrineCache(CacheProvider $cache): void
116
    {
117
        $this->roPool = new TagAwareAdapter(new DoctrineAdapter($cache));
118 26
        $this->etagPool = $this->roPool;
119
    }
120 26
121 8
    /**
122
     * {@inheritdoc}
123 18
     */
124 18
    public function get(AbstractUri $uri): ?ResourceState
125 2
    {
126
        $item = $this->roPool->getItem($this->getUriKey($uri, self::KEY_RO));
127
        assert($item instanceof ItemInterface);
128
        $state = $item->get();
129 18
        assert($state instanceof ResourceState || $state === null);
130
131
        return $state;
132 27
    }
133
134 27
    public function getDonut(AbstractUri $uri): ?ResourceDonut
135 18
    {
136
        $key = $this->getUriKey($uri, self::KEY_DONUT);
137 10
        $item = $this->roPool->getItem($key);
138 10
        assert($item instanceof ItemInterface);
139 10
        $donut = $item->get();
140 10
        assert($donut instanceof ResourceDonut || $donut === null);
141 10
142 10
        return $donut;
143
    }
144
145
    /**
146 10
     * {@inheritdoc}
147
     */
148
    public function hasEtag(string $etag): bool
149
    {
150
        return $this->etagPool->hasItem($etag);
151
    }
152
153
    /**
154
     * {@inheritdoc}
155
     */
156
    public function deleteEtag(AbstractUri $uri)
157
    {
158
        $uriTag = ($this->uriTag)($uri);
159
160
        return $this->invalidateTags([$uriTag]);
161
    }
162
163
    /**
164
     * {@inheritdoc}
165
     */
166
    public function invalidateTags(array $tags): bool
167
    {
168
        $tag = $tags ? implode(' ', $tags) : '';
169
        $this->logger->log('invalidate-etag tags:%s', $tag);
170
        $valid1 = $this->roPool->invalidateTags($tags);
171
        $valid2 = $this->etagPool->invalidateTags($tags);
172
        ($this->purger)(implode(' ', $tags));
173
174
        return $valid1 && $valid2;
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     *
180
     * @return bool
181
     */
182
    public function saveValue(ResourceObject $ro, int $ttl)
183
    {
184
        /** @psalm-suppress MixedAssignment $body */
185
        $body = $this->evaluateBody($ro->body);
186
        $value = ResourceState::create($ro, $body, null);
187
        $key = $this->getUriKey($ro->uri, self::KEY_RO);
188
        $tags = $this->getTags($ro);
189
        $this->logger->log('save-value uri:%s tags:%s ttl:%s', $ro->uri, $tags, $ttl);
190
191
        return $this->saver->__invoke($key, $value, $this->roPool, $ro->uri, $tags, $ttl);
192
    }
193
194
    /**
195
     * {@inheritdoc}
196
     *
197
     * @return bool
198
     */
199
    public function saveView(ResourceObject $ro, int $ttl)
200
    {
201
        $this->logger->log('save-view uri:%s ttl:%s', $ro->uri, $ttl);
202
        /** @psalm-suppress MixedAssignment $body */
203
        $body = $this->evaluateBody($ro->body);
204
        $value = ResourceState::create($ro, $body, $ro->view);
205
        $key = $this->getUriKey($ro->uri, self::KEY_RO);
206
        $tags = $this->getTags($ro);
207
208
        return $this->saver->__invoke($key, $value, $this->roPool, $ro->uri, $tags, $ttl);
209
    }
210
211
    public function saveDonut(AbstractUri $uri, ResourceDonut $donut, ?int $sMaxAge): void
212
    {
213
        $key = $this->getUriKey($uri, self::KEY_DONUT);
214
        $this->logger->log('save-donut uri:%s s-maxage:%s', $uri, $sMaxAge);
215
216
        $this->saver->__invoke($key, $donut, $this->roPool, $uri, [], $sMaxAge);
217
    }
218
219
    public function saveDonutView(ResourceObject $ro, ?int $ttl): bool
220
    {
221
        $resourceState = ResourceState::create($ro, [], $ro->view);
222
        $key = $this->getUriKey($ro->uri, self::KEY_RO);
223
        $tags = $this->getTags($ro);
224
        $this->logger->log('save-donut-view uri:%s surrogate-keys:%s s-maxage:%s', $ro->uri, $tags, $ttl);
225
226
        return $this->saver->__invoke($key, $resourceState, $this->roPool, $ro->uri, $tags, $ttl);
227
    }
228
229
    /**
230
     * @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...
231
     */
232
    private function getTags(ResourceObject $ro): array
233
    {
234
        $etag = $ro->headers['ETag'];
235
        $tags = [$etag, ($this->uriTag)($ro->uri)];
236
        if (isset($ro->headers[Header::SURROGATE_KEY])) {
237
            $tags = array_merge($tags, explode(' ', $ro->headers[Header::SURROGATE_KEY]));
238
        }
239
240
        /** @var list<string> $uniqueTags */
241
        $uniqueTags = array_unique($tags);
242
243
        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...
244
    }
245
246
    /**
247
     * @param mixed $body
248
     *
249
     * @return mixed
250
     */
251
    private function evaluateBody($body)
252
    {
253
        if (! is_array($body)) {
254
            return $body;
255
        }
256
257
        /** @psalm-suppress MixedAssignment $item */
258
        foreach ($body as &$item) {
259
            if ($item instanceof RequestInterface) {
260
                $item = ($item)();
261
            }
262
263
            if ($item instanceof ResourceObject) {
264
                $item->body = $this->evaluateBody($item->body);
265
            }
266
        }
267
268
        return $body;
269
    }
270
271
    private function getUriKey(AbstractUri $uri, string $type): string
272
    {
273
        return $type . ($this->uriTag)($uri) . (isset($_SERVER['X_VARY']) ? $this->getVary() : '');
274
    }
275
276
    private function getVary(): string
277
    {
278
        $xvary = $_SERVER['X_VARY'];
279
        assert(is_string($xvary));
280
        $varys = explode(',', $xvary);
281
        $varyString = '';
282
        foreach ($varys as $vary) {
283
            $phpVaryKey = sprintf('X_%s', strtoupper($vary));
284
            if (isset($_SERVER[$phpVaryKey]) && is_string($_SERVER[$phpVaryKey])) {
285
                $varyString .= $_SERVER[$phpVaryKey];
286
            }
287
        }
288
289
        return $varyString;
290
    }
291
292
    public function saveEtag(AbstractUri $uri, string $etag, string $surrogateKeys, ?int $ttl): void
293
    {
294
        $tags = $surrogateKeys ? explode(' ', $surrogateKeys) : [];
295
        $tags[] = (new UriTag())($uri);
296
        /** @var list<string> $uniqueTags */
297
        $uniqueTags = array_unique($tags);
298
        $this->logger->log('save-etag uri:%s etag:%s surrogate-keys:%s', $uri, $etag, $uniqueTags);
299
        $this->saver->__invoke($etag, 'etag', $this->etagPool, $uri, $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

299
        $this->saver->__invoke($etag, 'etag', $this->etagPool, $uri, /** @scrutinizer ignore-type */ $uniqueTags, $ttl);
Loading history...
300
    }
301
302
    /**
303
     * @return array{logger: RepositoryLoggerInterface, purger: PurgerInterface, uriTag: UriTagInterface, saver: ResourceStorageSaver, knownTagTtl: float, injector: InjectorInterface}
304
     */
305
    public function __serialize(): array
306
    {
307
        return [
308
            'logger' => $this->logger,
309
            'purger' => $this->purger,
310
            'uriTag' => $this->uriTag,
311
            'saver' => $this->saver,
312
            'knownTagTtl' => $this->knownTagTtl,
313
            'injector' => $this->injector,
314
        ];
315
    }
316
317
    /**
318
     * @param array{logger: RepositoryLoggerInterface, purger: PurgerInterface, uriTag: UriTagInterface, saver: ResourceStorageSaver, knownTagTtl: float, injector: InjectorInterface} $data
319
     */
320
    public function __unserialize(array $data): void
321
    {
322
        $this->logger = $data['logger'];
323
        $this->purger = $data['purger'];
324
        $this->uriTag = $data['uriTag'];
325
        $this->saver = $data['saver'];
326
        $pool = $data['injector']->getInstance(CacheItemPoolInterface::class, Shared::class);
327
        $etagPool = $data['injector']->getInstance(CacheItemPoolInterface::class, EtagPool::class);
328
        assert($pool instanceof AdapterInterface);
329
        assert($etagPool instanceof AdapterInterface);
330
        $this->roPool = new TagAwareAdapter($pool, $etagPool, $data['knownTagTtl']);
331
        $this->etagPool = new TagAwareAdapter($etagPool, $etagPool, $data['knownTagTtl']);
332
    }
333
}
334