ResourceStorage   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 263
Duplicated Lines 0 %

Importance

Changes 15
Bugs 0 Features 0
Metric Value
wmc 33
eloc 103
c 15
b 0
f 0
dl 0
loc 263
rs 9.76

18 Methods

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

274
        $this->saver->__invoke($sanitizedEtag, 'etag', $this->etagPool, /** @scrutinizer ignore-type */ $uniqueTags, $ttl);
Loading history...
275
    }
276
277
    public function __serialize(): array
278
    {
279
        return [
280
            'logger' => $this->logger,
281
            'purger' => $this->purger,
282
            'uriTag' => $this->uriTag,
283
            'saver' => $this->saver,
284
            'roProvider' => $this->roPoolProvider,
285
            'etagProvider' => $this->etagPoolProvider,
286
        ];
287
    }
288
289
    /**
290
     * @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...
291
     *
292
     * @return void
293
     */
294
    public function __unserialize(array $data): void
295
    {
296
        $this->logger = $data['logger'];
297
        $this->purger = $data['purger'];
298
        $this->uriTag = $data['uriTag'];
299
        $this->saver = $data['saver'];
300
        $this->initializePools($data['roProvider'], $data['etagProvider']);
301
    }
302
}
303