Passed
Pull Request — master (#19188)
by Alexander
56:39 queued 19:53
created

PageCache::calculateCacheKey()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 7
ccs 0
cts 0
cp 0
crap 6
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\filters;
9
10
use Yii;
11
use yii\base\Action;
12
use yii\base\ActionFilter;
13
use yii\base\DynamicContentAwareInterface;
14
use yii\base\DynamicContentAwareTrait;
15
use yii\caching\CacheInterface;
16
use yii\caching\Dependency;
17
use yii\di\Instance;
18
use yii\web\Response;
19
20
/**
21
 * PageCache implements server-side caching of whole pages.
22
 *
23
 * It is an action filter that can be added to a controller and handles the `beforeAction` event.
24
 *
25
 * To use PageCache, declare it in the `behaviors()` method of your controller class.
26
 * In the following example the filter will be applied to the `index` action and
27
 * cache the whole page for maximum 60 seconds or until the count of entries in the post table changes.
28
 * It also stores different versions of the page depending on the application language.
29
 *
30
 * ```php
31
 * public function behaviors()
32
 * {
33
 *     return [
34
 *         'pageCache' => [
35
 *             'class' => 'yii\filters\PageCache',
36
 *             'only' => ['index'],
37
 *             'duration' => 60,
38
 *             'dependency' => [
39
 *                 'class' => 'yii\caching\DbDependency',
40
 *                 'sql' => 'SELECT COUNT(*) FROM post',
41
 *             ],
42
 *             'variations' => [
43
 *                 \Yii::$app->language,
44
 *             ]
45
 *         ],
46
 *     ];
47
 * }
48
 * ```
49
 *
50
 * @author Qiang Xue <[email protected]>
51
 * @author Sergey Makinen <[email protected]>
52
 * @since 2.0
53
 */
54
class PageCache extends ActionFilter implements DynamicContentAwareInterface
55
{
56
    use DynamicContentAwareTrait;
57
58
    /**
59
     * Page cache version, to detect incompatibilities in cached values when the
60
     * data format of the cache changes.
61
     */
62
    const PAGE_CACHE_VERSION = 1;
63
64
    /**
65
     * @var bool whether the content being cached should be differentiated according to the route.
66
     * A route consists of the requested controller ID and action ID. Defaults to `true`.
67
     */
68
    public $varyByRoute = true;
69
    /**
70
     * @var CacheInterface|array|string the cache object or the application component ID of the cache object.
71
     * After the PageCache object is created, if you want to change this property,
72
     * you should only assign it with a cache object.
73
     * Starting from version 2.0.2, this can also be a configuration array for creating the object.
74
     */
75
    public $cache = 'cache';
76
    /**
77
     * @var int number of seconds that the data can remain valid in cache.
78
     * Use `0` to indicate that the cached data will never expire.
79
     */
80
    public $duration = 60;
81
    /**
82
     * @var array|Dependency the dependency that the cached content depends on.
83
     * This can be either a [[Dependency]] object or a configuration array for creating the dependency object.
84
     * For example,
85
     *
86
     * ```php
87
     * [
88
     *     'class' => 'yii\caching\DbDependency',
89
     *     'sql' => 'SELECT MAX(updated_at) FROM post',
90
     * ]
91
     * ```
92
     *
93
     * would make the output cache depend on the last modified time of all posts.
94
     * If any post has its modification time changed, the cached content would be invalidated.
95
     *
96
     * If [[cacheCookies]] or [[cacheHeaders]] is enabled, then [[\yii\caching\Dependency::reusable]] should be enabled as well to save performance.
97
     * This is because the cookies and headers are currently stored separately from the actual page content, causing the dependency to be evaluated twice.
98
     */
99
    public $dependency;
100
    /**
101
     * @var string[]|string list of factors that would cause the variation of the content being cached.
102
     * Each factor is a string representing a variation (e.g. the language, a GET parameter).
103
     * The following variation setting will cause the content to be cached in different versions
104
     * according to the current application language:
105
     *
106
     * ```php
107
     * [
108
     *     Yii::$app->language,
109
     * ]
110
     * ```
111
     */
112
    public $variations;
113
    /**
114
     * @var bool whether to enable the page cache. You may use this property to turn on and off
115
     * the page cache according to specific setting (e.g. enable page cache only for GET requests).
116
     */
117
    public $enabled = true;
118
    /**
119
     * @var \yii\base\View the view component to use for caching. If not set, the default application view component
120
     * [[\yii\web\Application::view]] will be used.
121
     */
122
    public $view;
123
    /**
124
     * @var bool|array a boolean value indicating whether to cache all cookies, or an array of
125
     * cookie names indicating which cookies can be cached. Be very careful with caching cookies, because
126
     * it may leak sensitive or private data stored in cookies to unwanted users.
127
     * @since 2.0.4
128
     */
129
    public $cacheCookies = false;
130
    /**
131
     * @var bool|array a boolean value indicating whether to cache all HTTP headers, or an array of
132
     * HTTP header names (case-sensitive) indicating which HTTP headers can be cached.
133
     * Note if your HTTP headers contain sensitive information, you should white-list which headers can be cached.
134
     * @since 2.0.4
135
     */
136
    public $cacheHeaders = true;
137
138
139
    /**
140
     * {@inheritdoc}
141
     */
142
    public function init()
143
    {
144 15
        parent::init();
145
        if ($this->view === null) {
146 15
            $this->view = Yii::$app->getView();
147 15
        }
148 2
    }
149
150 15
    /**
151
     * This method is invoked right before an action is to be executed (after all possible filters.)
152
     * You may override this method to do last-minute preparation for the action.
153
     * @param Action $action the action to be executed.
154
     * @return bool whether the action should continue to be executed.
155
     */
156
    public function beforeAction($action)
157
    {
158 13
        if (!$this->enabled) {
159
            return true;
160 13
        }
161 1
162
        $this->cache = Instance::ensure($this->cache, 'yii\caching\CacheInterface');
163
164 12
        if (is_array($this->dependency)) {
165
            $this->dependency = Yii::createObject($this->dependency);
166 12
        }
167 1
168
        $response = Yii::$app->getResponse();
169
        $data = $this->cache->get($this->calculateCacheKey());
170 12
        if (!is_array($data) || !isset($data['cacheVersion']) || $data['cacheVersion'] !== static::PAGE_CACHE_VERSION) {
171 12
            $this->view->pushDynamicContent($this);
172 12
            ob_start();
173 12
            ob_implicit_flush(false);
174 12
            $response->on(Response::EVENT_AFTER_SEND, [$this, 'cacheResponse']);
175 12
            Yii::debug('Valid page content is not found in the cache.', __METHOD__);
176 12
            return true;
177 12
        }
178 12
179
        $this->restoreResponse($response, $data);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type yii\console\Response; however, parameter $response of yii\filters\PageCache::restoreResponse() does only seem to accept yii\web\Response, 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

179
        $this->restoreResponse(/** @scrutinizer ignore-type */ $response, $data);
Loading history...
180
        Yii::debug('Valid page content is found in the cache.', __METHOD__);
181 11
        return false;
182 11
    }
183 11
184
    /**
185
     * This method is invoked right before the response caching is to be started.
186
     * You may override this method to cancel caching by returning `false` or store an additional data
187
     * in a cache entry by returning an array instead of `true`.
188
     * @return bool|array whether to cache or not, return an array instead of `true` to store an additional data.
189
     * @since 2.0.11
190
     */
191
    public function beforeCacheResponse()
192
    {
193 12
        return true;
194
    }
195 12
196
    /**
197
     * This method is invoked right after the response restoring is finished (but before the response is sent).
198
     * You may override this method to do last-minute preparation before the response is sent.
199
     * @param array|null $data an array of an additional data stored in a cache entry or `null`.
200
     * @since 2.0.11
201
     */
202
    public function afterRestoreResponse($data)
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed. ( Ignorable by Annotation )

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

202
    public function afterRestoreResponse(/** @scrutinizer ignore-unused */ $data)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
203
    {
204 11
    }
205
206 11
    /**
207
     * Restores response properties from the given data.
208
     * @param Response $response the response to be restored.
209
     * @param array $data the response property data.
210
     * @since 2.0.3
211
     */
212
    protected function restoreResponse($response, $data)
213
    {
214 11
        foreach (['format', 'version', 'statusCode', 'statusText', 'content'] as $name) {
215
            $response->{$name} = $data[$name];
216 11
        }
217 11
        foreach (['headers', 'cookies'] as $name) {
218
            if (isset($data[$name]) && is_array($data[$name])) {
219 11
                $response->{$name}->fromArray(array_merge($data[$name], $response->{$name}->toArray()));
220 11
            }
221 11
        }
222
        if (!empty($data['dynamicPlaceholders']) && is_array($data['dynamicPlaceholders'])) {
223
            $response->content = $this->updateDynamicContent($response->content, $data['dynamicPlaceholders'], true);
224 11
        }
225 11
        $this->afterRestoreResponse(isset($data['cacheData']) ? $data['cacheData'] : null);
226
    }
227 11
228 11
    /**
229
     * Caches response properties.
230
     * @since 2.0.3
231
     */
232
    public function cacheResponse()
233
    {
234 12
        $this->view->popDynamicContent();
235
        $beforeCacheResponseResult = $this->beforeCacheResponse();
236 12
        if ($beforeCacheResponseResult === false) {
0 ignored issues
show
introduced by
The condition $beforeCacheResponseResult === false is always false.
Loading history...
237 12
            echo $this->updateDynamicContent(ob_get_clean(), $this->getDynamicPlaceholders());
238 12
            return;
239
        }
240
241
        $response = Yii::$app->getResponse();
242
        $response->off(Response::EVENT_AFTER_SEND, [$this, 'cacheResponse']);
243 12
        $data = [
244 12
            'cacheVersion' => static::PAGE_CACHE_VERSION,
245
            'cacheData' => is_array($beforeCacheResponseResult) ? $beforeCacheResponseResult : null,
0 ignored issues
show
introduced by
The condition is_array($beforeCacheResponseResult) is always false.
Loading history...
246 12
            'content' => ob_get_clean(),
247 12
        ];
248 12
        if ($data['content'] === false || $data['content'] === '') {
249
            return;
250 12
        }
251 4
252
        $data['dynamicPlaceholders'] = $this->getDynamicPlaceholders();
253
        foreach (['format', 'version', 'statusCode', 'statusText'] as $name) {
254 12
            $data[$name] = $response->{$name};
255 12
        }
256 12
        $this->insertResponseHeaderCollectionIntoData($response, $data);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type yii\console\Response; however, parameter $response of yii\filters\PageCache::i...derCollectionIntoData() does only seem to accept yii\web\Response, 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

256
        $this->insertResponseHeaderCollectionIntoData(/** @scrutinizer ignore-type */ $response, $data);
Loading history...
257
        $this->insertResponseCookieCollectionIntoData($response, $data);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type yii\console\Response; however, parameter $response of yii\filters\PageCache::i...kieCollectionIntoData() does only seem to accept yii\web\Response, 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

257
        $this->insertResponseCookieCollectionIntoData(/** @scrutinizer ignore-type */ $response, $data);
Loading history...
258 12
        $this->cache->set($this->calculateCacheKey(), $data, $this->duration, $this->dependency);
0 ignored issues
show
Bug introduced by
It seems like $this->dependency can also be of type array; however, parameter $dependency of yii\caching\CacheInterface::set() does only seem to accept yii\caching\Dependency, 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

258
        $this->cache->set($this->calculateCacheKey(), $data, $this->duration, /** @scrutinizer ignore-type */ $this->dependency);
Loading history...
259 12
        $data['content'] = $this->updateDynamicContent($data['content'], $this->getDynamicPlaceholders());
260 12
        echo $data['content'];
261 12
    }
262 12
263 12
    /**
264
     * Inserts (or filters/ignores according to config) response cookies into a cache data array.
265
     * @param Response $response the response.
266
     * @param array $data the cache data.
267
     */
268
    private function insertResponseCookieCollectionIntoData(Response $response, array &$data)
269
    {
270
        if ($this->cacheCookies === false) {
271 12
            return;
272
        }
273 12
274 12
        $all = $response->cookies->toArray();
275 9
        if (is_array($this->cacheCookies)) {
276
            $filtered = [];
277
            foreach ($this->cacheCookies as $name) {
278 11
                if (isset($all[$name])) {
279 11
                    $filtered[$name] = $all[$name];
280 3
                }
281 3
            }
282 3
            $all = $filtered;
283 2
        }
284
        $data['cookies'] = $all;
285 3
    }
286 3
287
    /**
288
     * Inserts (or filters/ignores according to config) response headers into a cache data array.
289 3
     * @param Response $response the response.
290
     * @param array $data the cache data.
291 11
     */
292 11
    private function insertResponseHeaderCollectionIntoData(Response $response, array &$data)
293
    {
294
        if ($this->cacheHeaders === false) {
295
            return;
296
        }
297
298 13
        $all = $response->headers->toArray(true);
299
        if (is_array($this->cacheHeaders)) {
300 13
            $filtered = [];
301 13
            foreach ($this->cacheHeaders as $name) {
302 13
                if (isset($all[$name])) {
303
                    $filtered[$name] = $all[$name];
304 13
                }
305
            }
306
            $all = $filtered;
307
        }
308
        $data['headers'] = $all;
309
    }
310 12
311
    /**
312 12
     * @return array the key used to cache response properties.
313
     * @since 2.0.3
314
     */
315
    protected function calculateCacheKey()
316
    {
317
        $key = [__CLASS__];
318
        if ($this->varyByRoute) {
319
            $key[] = Yii::$app->requestedRoute;
320
        }
321
        return array_merge($key, (array)$this->variations);
322
    }
323
324
    /**
325
     * {@inheritdoc}
326
     */
327
    public function getView()
328
    {
329
        return $this->view;
330
    }
331
}
332