Passed
Pull Request — master (#91)
by
unknown
13:24
created

FragmentCache::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
nc 1
nop 2
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
54
     */
55
    private string $key;
56
57
    /**
58
     * @var LoggerInterface
59
     */
60
    private LoggerInterface $logger;
61
62
    /**
63
     * @var bool[]
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 = []): FragmentCacheInterface
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
    public function endCache(): void
136
    {
137
        if ($this->status === self::STATUS_IN_CACHE) {
138
            $this->status = self::STATUS_END;
139
            $this->logger->debug("Rendering Fragment (from cache): {$this->id}");
140
            echo $this->content;
141
            $this->clearData();
142
            return;
143
        }
144
        if ($this->status !== self::STATUS_BEGIN) {
145
            throw new RuntimeException('This method is not available in the current state.');
146
        }
147
        $this->status = self::STATUS_END;
148
        if ($this->savedObLevel === self::NOCACHE) {
149
            $this->logger->debug("Rendering Fragment (cache disabled): {$this->id}");
150
            return;
151
        }
152
        $this->logger->debug("Rendering Fragment (miss): {$this->id}");
153
        $content = $this->getCachedContent();
154
        if ($this->view) {
155
            $this->view->popDynamicContent($this);
156
        }
157
        if ($this->enabled && $this->cache) {
158
            if ($this->cache instanceof YiiCacheInterface) {
159
                $this->cache->set($this->key, [$content, $this->renderVars, $this->getDynamicPlaceholders()], $this->duration, $this->dependency);
0 ignored issues
show
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

159
                $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...
160
            } else {
161
                $this->cache->set($this->key, [$content, $this->renderVars, $this->getDynamicPlaceholders()], $this->duration);
162
            }
163
        }
164
        $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

164
        /** @scrutinizer ignore-call */ 
165
        $content = $this->updateContent($content, $this->getDynamicPlaceholders());
Loading history...
165
        echo $content;
166
        $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

166
        $this->/** @scrutinizer ignore-call */ 
167
               clearData();
Loading history...
167
    }
168
169
    public function getStatus(): int
170
    {
171
        return $this->status;
172
    }
173
174
    public function renderVar(string $name): string
175
    {
176
        if ($this->savedObLevel === self::NOCACHE) {
177
            return \strval($this->vars[$name] ?? '');
178
        }
179
        $this->renderVars[$name] = true;
180
        return "<![CDATA[YII-VAR-$name]]>";
181
    }
182
183
    protected function getView(): View
184
    {
185
        if (!$this->view) {
186
            throw new RuntimeException('The view is undefined.');
187
        }
188
        return $this->view;
189
    }
190
191
    /**
192
     * Builds a normalized cache key from a given key.
193
     *
194
     * If the given key is a string containing alphanumeric characters only and no more than 32 characters,
195
     * then the key will be returned back as it is, integers will be converted to strings. Otherwise, a normalized key
196
     * is generated by serializing the given key and applying MD5 hashing.
197
     * @param mixed $key the key to be normalized
198
     * @return string the generated cache key
199
     */
200
    private function buildKey($key): string
201
    {
202
        $jsonKey = \json_encode($key);
203
        if ($jsonKey === false) {
204
            throw new InvalidArgumentException('Invalid key. ' . \json_last_error_msg());
205
        }
206
        return \md5($jsonKey);
207
    }
208
209
    /**
210
     * Clearing memory after caching is complete.
211
     */
212
    private function clearData()
213
    {
214
        $this->content = '';
215
        $this->dependency = null;
216
        $this->id = '';
217
        $this->key = '';
218
        $this->renderVars = [];
219
        $this->vars = [];
220
        $this->view = null;
221
        $this->setDynamicPlaceholders([]);
222
    }
223
224
    private function getCachedContent(): string
225
    {
226
        if (\ob_get_level() < $this->savedObLevel) {
227
            throw new RuntimeException('The cache level is less than expected.');
228
        }
229
        while (\ob_get_level() > $this->savedObLevel) {
230
            if (!\ob_end_flush()) {
231
                if (!\is_string($content = \ob_get_clean())) {
232
                    throw new RuntimeException('It is not possible to delete a higher-level cache.');
233
                }
234
                echo $content;
235
            }
236
        }
237
        return \ob_get_clean() ?: '';
238
    }
239
240
    private function readFromCache(): bool
241
    {
242
        if (!$this->cache || !\is_array($value = $this->cache->get($this->key)) || \count($value) !== 3) {
243
            return false;
244
        }
245
        [$content, $renderVars, $placeholders] = $value;
246
        if (!\is_string($content) || !\is_array($renderVars) || !\is_array($placeholders)) {
247
            return false;
248
        }
249
        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...
250
            return false;
251
        }
252
        $this->renderVars = $renderVars;
253
        $this->content = $this->updateContent($content, $placeholders, true);
254
        return true;
255
    }
256
257
    private function startCache(): void
258
    {
259
        if (!\ob_start()) {
260
            return;
261
        }
262
        \ob_implicit_flush(0);
263
        $this->savedObLevel = \ob_get_level();
264
        if ($this->view) {
265
            $this->view->pushDynamicContent($this);
266
        }
267
    }
268
269
    private function updateContent(string $content, array $placeholders, bool $isRestoredFromCache = false): string
270
    {
271
        $content = $this->updateVarContent($content);
272
        if ($this->view) {
273
            $content = $this->updateDynamicContent($content, $placeholders, $isRestoredFromCache);
274
        }
275
        return $content;
276
    }
277
278
    private function updateVarContent(string $content): string
279
    {
280
        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...
281
            return $content;
282
        }
283
        $vars = [];
284
        foreach (\array_merge($this->renderVars, \array_keys($this->vars)) as $name => $_) {
285
            $vars["<![CDATA[YII-VAR-$name]]>"] = \strval($this->vars[$name] ?? '');
286
        }
287
        return \strtr($content, $vars);
288
    }
289
}
290