ResourceStorage   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 248
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 17
Bugs 1 Features 1
Metric Value
eloc 91
dl 0
loc 248
ccs 50
cts 50
cp 1
rs 9.76
c 17
b 1
f 1
wmc 33

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __unserialize() 0 7 1
A invalidateTags() 0 9 3
A __serialize() 0 9 1
A saveDonutView() 0 8 1
A saveDonut() 0 6 1
A initializePools() 0 7 2
A hasEtag() 0 3 1
A evaluateBody() 0 18 5
A deleteEtag() 0 5 1
A saveEtag() 0 8 2
A saveValue() 0 10 1
A __construct() 0 11 1
A getTags() 0 12 2
A getDonut() 0 8 2
A get() 0 7 2
A saveView() 0 10 1
A getVary() 0 13 4
A getUriKey() 0 3 2
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\ResourceObjectPool;
9
use BEAR\Resource\AbstractUri;
10
use BEAR\Resource\RequestInterface;
11
use BEAR\Resource\ResourceObject;
12
use Ray\Di\Di\Set;
13
use Ray\Di\ProviderInterface;
14
use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
15
16
use function array_merge;
17
use function array_unique;
18
use function assert;
19
use function explode;
20
use function implode;
21
use function is_array;
22
use function is_string;
23
use function sprintf;
24
use function strtoupper;
25
26
/**
27
 * @psalm-type Props = array{
28
 *     logger: RepositoryLoggerInterface,
29
 *     purger:PurgerInterface,
30
 *     uriTag: UriTag,
31
 *     saver: ResourceStorageSaver,
32
 *     roProvider:ProviderInterface<TagAwareAdapterInterface>,
33 30
 *     etagProvider: ProviderInterface<TagAwareAdapterInterface>
34
 * }
35 30
 */
36 30
final class ResourceStorage implements ResourceStorageInterface
37
{
38
    /**
39
     * Resource object cache prefix
40
     */
41 3
    private const KEY_RO = 'ro-';
42
43 3
    /**
44
     * Resource static cache prifix
45
     */
46
    private const KEY_DONUT = 'donut-';
47
48
    /** @var ProviderInterface<TagAwareAdapterInterface> */
49 26
    private ProviderInterface $roPoolProvider;
50
51 26
    /** @var ProviderInterface<TagAwareAdapterInterface> */
52 26
    private ProviderInterface $etagPoolProvider;
53 26
    private TagAwareAdapterInterface $roPool;
54
    private TagAwareAdapterInterface $etagPool;
55 26
56
    /**
57 26
     * @param ProviderInterface<TagAwareAdapterInterface> $roPoolProvider
58
     * @param ProviderInterface<TagAwareAdapterInterface> $etagPoolProvider
59 26
     */
60 26
    public function __construct(
61
        private RepositoryLoggerInterface $logger,
62
        private PurgerInterface $purger,
63
        private UriTagInterface $uriTag,
64
        private ResourceStorageSaver $saver,
65 27
        #[Set(TagAwareAdapterInterface::class, ResourceObjectPool::class)]
66
        ProviderInterface $roPoolProvider,
67 27
        #[Set(TagAwareAdapterInterface::class, EtagPool::class)]
68 27
        ProviderInterface $etagPoolProvider,
69
    ) {
70 27
        $this->initializePools($roPoolProvider, $etagPoolProvider);
71 27
    }
72
73
    /**
74
     * @param ProviderInterface<TagAwareAdapterInterface> $roPoolProvider
75
     * @param ProviderInterface<TagAwareAdapterInterface> $etagPoolProvider
76 25
     */
77
    private function initializePools(ProviderInterface $roPoolProvider, ProviderInterface $etagPoolProvider): void
78 25
    {
79
        $this->roPoolProvider = $roPoolProvider;
80 25
        $this->etagPoolProvider = $etagPoolProvider;
81
        $this->roPool = $roPoolProvider->get();
82
        $etagPool = $this->etagPoolProvider->get();
83
        $this->etagPool = $etagPool instanceof TagAwareAdapterInterface ? $etagPool : $this->roPool; // @phpstan-ignore-line
84
    }
85
86 12
    /**
87
     * {@inheritDoc}
88 12
     */
89 12
    public function get(AbstractUri $uri): ResourceState|null
90
    {
91 12
        $item = $this->roPool->getItem($this->getUriKey($uri, self::KEY_RO));
92
        $state = $item->get();
93
        assert($state instanceof ResourceState || $state === null);
94
95
        return $state;
96
    }
97 24
98
    public function getDonut(AbstractUri $uri): ResourceDonut|null
99 24
    {
100 24
        $key = $this->getUriKey($uri, self::KEY_DONUT);
101 24
        $item = $this->roPool->getItem($key);
102
        $donut = $item->get();
103 24
        assert($donut instanceof ResourceDonut || $donut === null);
104
105
        return $donut;
106
    }
107
108
    /**
109 2
     * {@inheritDoc}
110
     */
111 2
    public function hasEtag(string $etag): bool
112 2
    {
113 2
        return $this->etagPool->hasItem($etag);
114
    }
115 2
116
    /**
117
     * {@inheritDoc}
118 26
     */
119
    public function deleteEtag(AbstractUri $uri)
120 26
    {
121 8
        $uriTag = ($this->uriTag)($uri);
122
123 18
        return $this->invalidateTags([$uriTag]);
124 18
    }
125 2
126
    /**
127
     * {@inheritDoc}
128
     */
129 18
    public function invalidateTags(array $tags): bool
130
    {
131
        $tag = $tags !== [] ? implode(' ', $tags) : '';
132 27
        $this->logger->log('invalidate-etag tags:%s', $tag);
133
        $valid1 = $this->roPool->invalidateTags($tags);
134 27
        $valid2 = $this->etagPool->invalidateTags($tags);
135 18
        ($this->purger)(implode(' ', $tags));
136
137 10
        return $valid1 && $valid2;
138 10
    }
139 10
140 10
    /**
141 10
     * {@inheritDoc}
142 10
     *
143
     * @return bool
144
     */
145
    public function saveValue(ResourceObject $ro, int $ttl)
146 10
    {
147
        /** @psalm-suppress MixedAssignment $body */
148
        $body = $this->evaluateBody($ro->body);
149
        $value = ResourceState::create($ro, $body, null);
150
        $key = $this->getUriKey($ro->uri, self::KEY_RO);
151
        $tags = $this->getTags($ro);
152
        $this->logger->log('save-value uri:%s tags:%s ttl:%s', $ro->uri, $tags, $ttl);
153
154
        return $this->saver->__invoke($key, $value, $this->roPool, $tags, $ttl);
155
    }
156
157
    /**
158
     * {@inheritDoc}
159
     *
160
     * @return bool
161
     */
162
    public function saveView(ResourceObject $ro, int $ttl)
163
    {
164
        $this->logger->log('save-view uri:%s ttl:%s', $ro->uri, $ttl);
165
        /** @psalm-suppress MixedAssignment $body */
166
        $body = $this->evaluateBody($ro->body);
167
        $value = ResourceState::create($ro, $body, $ro->view);
168
        $key = $this->getUriKey($ro->uri, self::KEY_RO);
169
        $tags = $this->getTags($ro);
170
171
        return $this->saver->__invoke($key, $value, $this->roPool, $tags, $ttl);
172
    }
173
174
    /**
175
     * {@inheritDoc}
176
     */
177
    public function saveDonut(AbstractUri $uri, ResourceDonut $donut, int|null $sMaxAge, array $headerKeys): void
178
    {
179
        $key = $this->getUriKey($uri, self::KEY_DONUT);
180
        $this->logger->log('save-donut uri:%s s-maxage:%s', $uri, $sMaxAge);
181
        $result = $this->saver->__invoke($key, $donut, $this->roPool, $headerKeys, $sMaxAge);
182
        assert($result, 'Donut save failed.');
183
    }
184
185
    public function saveDonutView(ResourceObject $ro, int|null $ttl): bool
186
    {
187
        $resourceState = ResourceState::create($ro, [], $ro->view);
188
        $key = $this->getUriKey($ro->uri, self::KEY_RO);
189
        $tags = $this->getTags($ro);
190
        $this->logger->log('save-donut-view uri:%s surrogate-keys:%s s-maxage:%s', $ro->uri, $tags, $ttl);
191
192
        return $this->saver->__invoke($key, $resourceState, $this->roPool, $tags, $ttl);
193
    }
194
195
    /** @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...
196
    private function getTags(ResourceObject $ro): array
197
    {
198
        $etag = $ro->headers['ETag'];
199
        $tags = [$etag, ($this->uriTag)($ro->uri)];
200
        if (isset($ro->headers[Header::SURROGATE_KEY])) {
201
            $tags = array_merge($tags, explode(' ', $ro->headers[Header::SURROGATE_KEY]));
202
        }
203
204
        /** @var list<string> $uniqueTags */
205
        $uniqueTags = array_unique($tags);
206
207
        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...
208
    }
209
210
    private function evaluateBody(mixed $body): mixed
211
    {
212
        if (! is_array($body)) {
213
            return $body;
214
        }
215
216
        /** @psalm-suppress MixedAssignment $item */
217
        foreach ($body as &$item) {
218
            if ($item instanceof RequestInterface) {
219
                $item = ($item)();
220
            }
221
222
            if ($item instanceof ResourceObject) {
223
                $item->body = $this->evaluateBody($item->body);
224
            }
225
        }
226
227
        return $body;
228
    }
229
230
    private function getUriKey(AbstractUri $uri, string $type): string
231
    {
232
        return $type . ($this->uriTag)($uri) . (isset($_SERVER['X_VARY']) ? $this->getVary() : '');
233
    }
234
235
    private function getVary(): string
236
    {
237
        $xvary = $_SERVER['X_VARY'];
238
        $varys = explode(',', $xvary); // @phpstan-ignore-line
239
        $varyString = '';
240
        foreach ($varys as $vary) {
241
            $phpVaryKey = sprintf('X_%s', strtoupper($vary));
242
            if (isset($_SERVER[$phpVaryKey]) && is_string($_SERVER[$phpVaryKey])) {
243
                $varyString .= $_SERVER[$phpVaryKey];
244
            }
245
        }
246
247
        return $varyString;
248
    }
249
250
    public function saveEtag(AbstractUri $uri, string $etag, string $surrogateKeys, int|null $ttl): void
251
    {
252
        $tags = $surrogateKeys !== '' ? explode(' ', $surrogateKeys) : [];
253
        $tags[] = (new UriTag())($uri);
254
        /** @var list<string> $uniqueTags */
255
        $uniqueTags = array_unique($tags);
256
        $this->logger->log('save-etag uri:%s etag:%s surrogate-keys:%s', $uri, $etag, $uniqueTags);
257
        $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

257
        $this->saver->__invoke($etag, 'etag', $this->etagPool, /** @scrutinizer ignore-type */ $uniqueTags, $ttl);
Loading history...
258
    }
259
260
    public function __serialize(): array
261
    {
262
        return [
263
            'logger' => $this->logger,
264
            'purger' => $this->purger,
265
            'uriTag' => $this->uriTag,
266
            'saver' => $this->saver,
267
            'roProvider' => $this->roPoolProvider,
268
            'etagProvider' => $this->etagPoolProvider,
269
        ];
270
    }
271
272
    /**
273
     * @param Props $data
0 ignored issues
show
Bug introduced by
The type BEAR\QueryRepository\Props 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...
274
     *
275
     * @return void
276
     */
277
    public function __unserialize(array $data): void
278
    {
279
        $this->logger = $data['logger'];
280
        $this->purger = $data['purger'];
281
        $this->uriTag = $data['uriTag'];
282
        $this->saver = $data['saver'];
283
        $this->initializePools($data['roProvider'], $data['etagProvider']);
284
    }
285
}
286