ResourceStorage   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 271
Duplicated Lines 0 %

Importance

Changes 17
Bugs 0 Features 0
Metric Value
eloc 108
c 17
b 0
f 0
dl 0
loc 271
rs 9.76
wmc 33

18 Methods

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

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