Completed
Push — 2.1 ( dc42a5...e82b9c )
by
unknown
12:47
created

PageCache::insertResponseCollectionIntoData()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7

Importance

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

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

Loading history...
200
    {
201 11
    }
202
203
    /**
204
     * Restores response properties from the given data.
205
     * @param Response $response the response to be restored.
206
     * @param array $data the response property data.
207
     * @since 2.0.3
208
     */
209 11
    protected function restoreResponse($response, $data)
210
    {
211 11
        foreach (['format', 'protocolVersion', 'statusCode', 'reasonPhrase', 'content'] as $name) {
212 11
            $response->{$name} = $data[$name];
213
        }
214
215 11
        if (isset($data['headers'])) {
216 10
            $response->setHeaders($data['headers']);
217
        }
218
219 11
        foreach (['cookies'] as $name) {
220 11
            if (isset($data[$name]) && is_array($data[$name])) {
221 11
                $response->{$name}->fromArray(array_merge($data[$name], $response->{$name}->toArray()));
222
            }
223
        }
224 11
        if (!empty($data['dynamicPlaceholders']) && is_array($data['dynamicPlaceholders'])) {
225 11
            if (empty($this->view->cacheStack)) {
226
                // outermost cache: replace placeholder with dynamic content
227 11
                $response->content = $this->updateDynamicContent($response->content, $data['dynamicPlaceholders']);
228
            }
229 11
            foreach ($data['dynamicPlaceholders'] as $name => $statements) {
230 11
                $this->view->addDynamicPlaceholder($name, $statements);
231
            }
232
        }
233 11
        $this->afterRestoreResponse(isset($data['cacheData']) ? $data['cacheData'] : null);
234 11
    }
235
236
    /**
237
     * Caches response properties.
238
     * @since 2.0.3
239
     */
240 12
    public function cacheResponse()
241
    {
242 12
        array_pop($this->view->cacheStack);
243 12
        $beforeCacheResponseResult = $this->beforeCacheResponse();
244 12
        if ($beforeCacheResponseResult === false) {
245
            $content = ob_get_clean();
246
            if (empty($this->view->cacheStack) && !empty($this->dynamicPlaceholders)) {
247
                $content = $this->updateDynamicContent($content, $this->dynamicPlaceholders);
248
            }
249
            echo $content;
250
            return;
251
        }
252
253 12
        $response = Yii::$app->getResponse();
254
        $data = [
255 12
            'cacheVersion' => 1,
256 12
            'cacheData' => is_array($beforeCacheResponseResult) ? $beforeCacheResponseResult : null,
257 12
            'content' => ob_get_clean(),
258
        ];
259 12
        if ($data['content'] === false || $data['content'] === '') {
260 4
            return;
261
        }
262
263 12
        $data['dynamicPlaceholders'] = $this->dynamicPlaceholders;
264
265 12
        $data = array_merge($data, [
266 12
            'format' => $response->format,
267 12
            'protocolVersion' => $response->getProtocolVersion(),
268 12
            'statusCode' => $response->getStatusCode(),
269 12
            'reasonPhrase' => $response->getReasonPhrase(),
270
        ]);
271
272 12
        $this->insertResponseCollectionIntoData($response, 'headers', $data);
273 12
        $this->insertResponseCollectionIntoData($response, 'cookies', $data);
274 12
        $this->cache->set($this->calculateCacheKey(), $data, $this->duration, $this->dependency);
275 12
        if (empty($this->view->cacheStack) && !empty($this->dynamicPlaceholders)) {
276 12
            $data['content'] = $this->updateDynamicContent($data['content'], $this->dynamicPlaceholders);
277
        }
278 12
        echo $data['content'];
279 12
    }
280
281
    /**
282
     * Inserts (or filters/ignores according to config) response headers/cookies into a cache data array.
283
     * @param Response $response the response.
284
     * @param string $collectionName currently it's `headers` or `cookies`.
285
     * @param array $data the cache data.
286
     */
287 12
    private function insertResponseCollectionIntoData(Response $response, $collectionName, array &$data)
288
    {
289 12
        $property = 'cache' . ucfirst($collectionName);
290 12
        if ($this->{$property} === false) {
291 9
            return;
292
        }
293
294 11
        $collection = $response->{$collectionName};
295 11
        $all = is_array($collection) ? $collection : $collection->toArray();
296 11
        if (is_array($this->{$property})) {
297 3
            $filtered = [];
298 3
            foreach ($this->{$property} as $name) {
299 3
                if ($collectionName === 'headers') {
300 2
                    $name = strtolower($name);
301
                }
302 3
                if (isset($all[$name])) {
303 3
                    $filtered[$name] = $all[$name];
304
                }
305
            }
306 3
            $all = $filtered;
307
        }
308 11
        $data[$collectionName] = $all;
309 11
    }
310
311
    /**
312
     * Replaces placeholders in content by results of evaluated dynamic statements.
313
     * @param string $content content to be parsed.
314
     * @param array $placeholders placeholders and their values.
315
     * @return string final content.
316
     * @since 2.0.11
317
     */
318 12
    protected function updateDynamicContent($content, $placeholders)
319
    {
320 12
        foreach ($placeholders as $name => $statements) {
321 12
            $placeholders[$name] = $this->view->evaluateDynamicContent($statements);
322
        }
323
324 12
        return strtr($content, $placeholders);
325
    }
326
327
    /**
328
     * @return array the key used to cache response properties.
329
     * @since 2.0.3
330
     */
331 12
    protected function calculateCacheKey()
332
    {
333 12
        $key = [__CLASS__];
334 12
        if ($this->varyByRoute) {
335 12
            $key[] = Yii::$app->requestedRoute;
336
        }
337 12
        if (is_array($this->variations)) {
338 1
            foreach ($this->variations as $value) {
339 1
                $key[] = $value;
340
            }
341
        }
342 12
        return $key;
343
    }
344
}
345