Passed
Pull Request — master (#91)
by Alexander
21:32 queued 06:31
created

FragmentCache::renderVar()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 7
rs 10
cc 2
nc 2
nop 1
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
    private ?CacheInterface $cache;
33
34
    private string $content = '';
35
36
    private ?Dependency $dependency = null;
37
38
    private string $id;
39
40
    private string $key;
41
42
    private LoggerInterface $logger;
43
44
    /**
45
     * @var bool[]
46
     */
47
    private array $renderVars;
48
49
    private int $savedObLevel;
50
51
    private int $status = self::STATUS_NO;
52
53
    /**
54
     * @var string[]
55
     */
56
    private array $vars;
57
58
    private ?View $view = null;
59
60
    public function __construct(?CacheInterface $cache, LoggerInterface $logger)
61
    {
62
        $this->cache = $cache;
63
        $this->logger = $logger;
64
    }
65
66
    public function __clone()
67
    {
68
        $this->clearData();
69
        $this->savedObLevel = self::NOCACHE;
70
        $this->status = self::STATUS_INIT;
71
    }
72
73
    public function beginCache(?View $view, string $id, array $params = [], array $vars = []): FragmentCacheInterface
74
    {
75
        $obj = clone $this;
76
        $obj->view = $view;
77
        $obj->id = $id;
78
        if (isset($params['duration'])) {
79
            $obj->duration = (int)$params['duration'];
80
        }
81
        if (isset($params['enabled'])) {
82
            $obj->enabled = $obj->enabled && (bool)$params['enabled'];
83
        }
84
        if (isset($params['cache'])) {
85
            $obj->cache = $params['cache'];
86
        }
87
        $obj->vars = $vars;
88
        if (!$obj->cache || !$obj->enabled) {
89
            $obj->status = self::STATUS_BEGIN;
90
            return $obj;
91
        }
92
        if (isset($params['dependency'])) {
93
            if (!($obj->cache instanceof YiiCacheInterface)) {
94
                throw new RuntimeException('Dependencies are available only for the cache supporting the interface Yiisoft\Cache\CacheInterface.');
95
            }
96
            $obj->dependency = $params['dependency'];
97
        }
98
        $obj->key = $obj->buildKey(isset($params['variations']) ? [$id, $params['variations']] : $id);
99
        if ($obj->readFromCache()) {
100
            $obj->status = self::STATUS_IN_CACHE;
101
            return $obj;
102
        }
103
        $obj->status = self::STATUS_BEGIN;
104
        $obj->startCache();
105
        return $obj;
106
    }
107
108
    public function endCache(): void
109
    {
110
        if ($this->status === self::STATUS_IN_CACHE) {
111
            $this->status = self::STATUS_END;
112
            $this->logger->debug("Rendering Fragment (from cache): {$this->id}");
113
            echo $this->content;
114
            $this->clearData();
115
            return;
116
        }
117
        if ($this->status !== self::STATUS_BEGIN) {
118
            throw new RuntimeException('This method is not available in the current state.');
119
        }
120
        $this->status = self::STATUS_END;
121
        if ($this->savedObLevel === self::NOCACHE) {
122
            $this->logger->debug("Rendering Fragment (cache disabled): {$this->id}");
123
            return;
124
        }
125
        $this->logger->debug("Rendering Fragment (miss): {$this->id}");
126
        $content = $this->getCachedContent();
127
        if ($this->view) {
128
            $this->view->popDynamicContent($this);
129
        }
130
        if ($this->enabled && $this->cache) {
131
            if ($this->cache instanceof YiiCacheInterface) {
132
                $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

132
                $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...
133
            } else {
134
                $this->cache->set($this->key, [$content, $this->renderVars, $this->getDynamicPlaceholders()], $this->duration);
135
            }
136
        }
137
        $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

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

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