Passed
Pull Request — master (#91)
by
unknown
11:43
created

FragmentCache::readFromCache()   B

Complexity

Conditions 8
Paths 4

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
c 1
b 0
f 0
dl 0
loc 15
rs 8.4444
cc 8
nc 4
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\View;
6
7
use InvalidArgumentException;
8
use Psr\Log\LoggerInterface;
9
use Psr\SimpleCache\CacheInterface;
10
use RuntimeException;
11
use Yiisoft\Cache\CacheInterface as YiiCacheInterface;
12
use Yiisoft\Cache\Dependency\Dependency;
13
14
class FragmentCache implements FragmentCacheInterface, DynamicContentAwareInterface
15
{
16
    use DynamicContentAwareTrait;
17
18
    private const NOCACHE = -1;
19
20
    /**
21
     * @var int number of seconds that the data can remain valid in cache.
22
     * Use 0 to indicate that the cached data will never expire.
23
     */
24
    public int $duration = 60;
25
26
    /**
27
     * @var bool whether to enable the fragment cache. You may use this property to turn on and off
28
     * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests).
29
     */
30
    public bool $enabled = true;
31
32
    /**
33
     * @var CacheInterface|null
34
     */
35
    private ?CacheInterface $cache;
36
37
    /**
38
     * @var string
39
     */
40
    private string $content = '';
41
42
    /**
43
     * @var Dependency|null
44
     */
45
    private ?Dependency $dependency = null;
46
47
    /**
48
     * @var string
49
     */
50
    private string $id;
51
52
    /**
53
     * @var string|array
54
     */
55
    private $key;
56
57
    /**
58
     * @var LoggerInterface
59
     */
60
    private LoggerInterface $logger;
61
62
    /**
63
     * @var string[]
64
     */
65
    private array $renderVars;
66
67
    /**
68
     * @var int
69
     */
70
    private int $savedObLevel;
71
72
    /**
73
     * @var int
74
     */
75
    private int $status = self::STATUS_NO;
76
77
    /**
78
     * @var string[]
79
     */
80
    private array $vars;
81
82
    /**
83
     * @var View|null
84
     */
85
    private ?View $view = null;
86
87
    public function __construct(?CacheInterface $cache, LoggerInterface $logger)
88
    {
89
        $this->cache = $cache;
90
        $this->logger = $logger;
91
    }
92
93
    public function __clone()
94
    {
95
        $this->clearData();
96
        $this->savedObLevel = self::NOCACHE;
97
        $this->status = self::STATUS_INIT;
98
    }
99
100
    public function beginCache(?View $view, string $id, array $params = [], array $vars = []): self
101
    {
102
        $obj = clone $this;
103
        $obj->view = $view;
104
        $obj->id = $id;
105
        if (isset($params['duration'])) {
106
            $obj->duration = (int)$params['duration'];
107
        }
108
        if (isset($params['enabled'])) {
109
            $obj->enabled = $obj->enabled && (bool)$params['enabled'];
110
        }
111
        if (isset($params['cache'])) {
112
            $obj->cache = $params['cache'];
113
        }
114
        $obj->vars = $vars;
115
        if (!$obj->cache || !$obj->enabled) {
116
            $obj->status = self::STATUS_BEGIN;
117
            return $obj;
118
        }
119
        if (isset($params['dependency'])) {
120
            if (!($obj->cache instanceof YiiCacheInterface)) {
121
                throw new RuntimeException('Dependencies are available only for the cache supporting the interface Yiisoft\Cache\CacheInterface.');
122
            }
123
            $obj->dependency = $params['dependency'];
124
        }
125
        $obj->key = $obj->buildKey(isset($params['variations']) ? [$id, $params['variations']] : $id);
126
        if ($obj->readFromCache()) {
127
            $obj->status = self::STATUS_IN_CACHE;
128
            return $obj;
129
        }
130
        $obj->status = self::STATUS_BEGIN;
131
        $obj->startCache();
132
        return $obj;
133
    }
134
135
    /**
136
     * @throws \Psr\SimpleCache\InvalidArgumentException
137
     */
138
    public function endCache(): void
139
    {
140
        if ($this->status === self::STATUS_IN_CACHE) {
141
            $this->status = self::STATUS_END;
142
            $this->logger->debug("Rendering Fragment (from cache): {$this->id}");
143
            echo $this->content;
144
            $this->clearData();
145
            return;
146
        }
147
        if ($this->status !== self::STATUS_BEGIN) {
148
            throw new RuntimeException('This method is not available in the current state.');
149
        }
150
        $this->status = self::STATUS_END;
151
        if ($this->savedObLevel === self::NOCACHE) {
152
            $this->logger->debug("Rendering Fragment (cache disabled): {$this->id}");
153
            return;
154
        }
155
        $this->logger->debug("Rendering Fragment (miss): {$this->id}");
156
        $content = $this->getCachedContent();
157
        if ($this->view) {
158
            $this->view->popDynamicContent($this);
159
        }
160
        if ($this->enabled) {
161
            if ($this->cache instanceof YiiCacheInterface) {
162
                $this->cache->set($this->key, [$content, $this->renderVars, $this->getDynamicPlaceholders()], $this->duration, $this->dependency);
0 ignored issues
show
Bug introduced by
The method set() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

162
                $this->cache->/** @scrutinizer ignore-call */ 
163
                              set($this->key, [$content, $this->renderVars, $this->getDynamicPlaceholders()], $this->duration, $this->dependency);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Unused Code introduced by
The call to Psr\SimpleCache\CacheInterface::set() has too many arguments starting with $this->dependency. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

162
                $this->cache->/** @scrutinizer ignore-call */ 
163
                              set($this->key, [$content, $this->renderVars, $this->getDynamicPlaceholders()], $this->duration, $this->dependency);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
The method getDynamicPlaceholders() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

162
                $this->cache->set($this->key, [$content, $this->renderVars, $this->/** @scrutinizer ignore-call */ getDynamicPlaceholders()], $this->duration, $this->dependency);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
It seems like $this->key can also be of type array; however, parameter $key of Psr\SimpleCache\CacheInterface::set() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

162
                $this->cache->set(/** @scrutinizer ignore-type */ $this->key, [$content, $this->renderVars, $this->getDynamicPlaceholders()], $this->duration, $this->dependency);
Loading history...
163
            } else {
164
                $this->cache->set($this->key, [$content, $this->renderVars, $this->getDynamicPlaceholders()], $this->duration);
165
            }
166
        }
167
        $content = $this->updateContent($content, $this->getDynamicPlaceholders());
0 ignored issues
show
Bug introduced by
The method updateContent() does not exist on Yiisoft\View\DynamicContentAwareInterface. It seems like you code against a sub-type of Yiisoft\View\DynamicContentAwareInterface such as Yiisoft\View\FragmentCache. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

167
        /** @scrutinizer ignore-call */ 
168
        $content = $this->updateContent($content, $this->getDynamicPlaceholders());
Loading history...
168
        echo $content;
169
        $this->clearData();
0 ignored issues
show
Bug introduced by
The method clearData() does not exist on Yiisoft\View\DynamicContentAwareInterface. It seems like you code against a sub-type of Yiisoft\View\DynamicContentAwareInterface such as Yiisoft\View\FragmentCache. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

169
        $this->/** @scrutinizer ignore-call */ 
170
               clearData();
Loading history...
170
    }
171
172
    public function getStatus(): int
173
    {
174
        return $this->status;
175
    }
176
177
    public function renderVar(string $name): string
178
    {
179
        if ($this->savedObLevel === self::NOCACHE) {
180
            return \strval($obj->vars[$name] ?? '');
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $obj seems to be never defined.
Loading history...
181
        }
182
        if (!\array_key_exists($name, $this->renderVars)) {
183
            $this->renderVars[$name] = true;
184
        }
185
        return "<![CDATA[YII-VAR-$name]]>";
186
    }
187
188
    protected function getView(): View
189
    {
190
        if (!$this->view) {
191
            throw new RuntimeException('The view is undefined.');
192
        }
193
        return $this->view;
194
    }
195
196
    /**
197
     * Builds a normalized cache key from a given key.
198
     *
199
     * If the given key is a string containing alphanumeric characters only and no more than 32 characters,
200
     * then the key will be returned back as it is, integers will be converted to strings. Otherwise, a normalized key
201
     * is generated by serializing the given key and applying MD5 hashing.
202
     * @param mixed $key the key to be normalized
203
     * @return string the generated cache key
204
     */
205
    private function buildKey($key): string
206
    {
207
        $jsonKey = \json_encode($key);
208
        if ($jsonKey === false) {
209
            throw new InvalidArgumentException('Invalid key. ' . \json_last_error_msg());
210
        }
211
        return \md5($jsonKey);
212
    }
213
214
    /**
215
     * Clearing memory after caching is complete.
216
     */
217
    private function clearData()
218
    {
219
        $this->content = '';
220
        $this->dependency = null;
221
        $this->id = '';
222
        $this->key = '';
223
        $this->renderVars = [];
224
        $this->vars = [];
225
        $this->view = null;
226
        $this->setDynamicPlaceholders([]);
227
    }
228
229
    private function getCachedContent(): string
230
    {
231
        if (\ob_get_level() < $this->savedObLevel) {
232
            throw new RuntimeException('The cache level is less than expected.');
233
        }
234
        while (\ob_get_level() > $this->savedObLevel) {
235
            if (!\ob_end_flush()) {
236
                if (!\is_string($content = \ob_get_clean())) {
237
                    throw new RuntimeException('It is not possible to delete a higher-level cache.');
238
                }
239
                echo $content;
240
            }
241
        }
242
        return \ob_get_clean() ?: '';
243
    }
244
245
    private function readFromCache(): bool
246
    {
247
        if (!\is_array($value = $this->cache->get($this->key)) || \count($value) !== 3) {
0 ignored issues
show
Bug introduced by
It seems like $this->key can also be of type array; however, parameter $key of Psr\SimpleCache\CacheInterface::get() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

247
        if (!\is_array($value = $this->cache->get(/** @scrutinizer ignore-type */ $this->key)) || \count($value) !== 3) {
Loading history...
248
            return false;
249
        }
250
        [$content, $renderVars, $placeholders] = $value;
251
        if (!\is_string($content) || !\is_array($renderVars) || !\is_array($placeholders)) {
252
            return false;
253
        }
254
        if ($placeholders && !$this->view) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $placeholders of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
255
            return false;
256
        }
257
        $this->renderVars = $renderVars;
258
        $this->content = $this->updateContent($content, $placeholders, true);
259
        return true;
260
    }
261
262
    private function startCache(): void
263
    {
264
        if (!\ob_start()) {
265
            return;
266
        }
267
        \ob_implicit_flush(0);
268
        $this->savedObLevel = \ob_get_level();
269
        if ($this->view) {
270
            $this->view->pushDynamicContent($this);
271
        }
272
    }
273
274
    private function updateContent(string $content, array $placeholders, bool $isRestoredFromCache = false): string
275
    {
276
        $content = $this->updateVarContent($content);
277
        if ($this->view) {
278
            $content = $this->updateDynamicContent($content, $placeholders, $isRestoredFromCache);
279
        }
280
        return $content;
281
    }
282
283
    private function updateVarContent(string $content): string
284
    {
285
        if (!$this->renderVars) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->renderVars of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
286
            return $content;
287
        }
288
        $vars = [];
289
        foreach (\array_merge($this->renderVars, \array_keys($this->vars)) as $name => $_) {
290
            $vars["<![CDATA[YII-VAR-$name]]>"] = \strval($this->vars[$name] ?? '');
291
        }
292
        return \strtr($content, $vars);
293
    }
294
}
295