Completed
Push — cache-closure ( 7d9053...380edd )
by Dmitry
35:54
created

PageCache::updateDynamicContent()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 2
crap 2
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\Cache;
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',
34
 *             'only' => ['index'],
35
 *             'duration' => 60,
36
 *             'dependency' => [
37
 *                 'class' => 'yii\caching\DbDependency',
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 Cache|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',
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 1
        }
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, Cache::className());
160
161 12
        if (is_array($this->dependency)) {
162 1
            $this->dependency = Yii::createObject($this->dependency);
163 1
        }
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::trace('Valid page content is not found in the cache.', __METHOD__);
173 12
            return true;
174
        } else {
175 11
            $this->restoreResponse($response, $data);
176 11
            Yii::trace('Valid page content is found in the cache.', __METHOD__);
177 11
            return false;
178
        }
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', 'version', 'statusCode', 'statusText', 'content'] as $name) {
212 11
            $response->{$name} = $data[$name];
213 11
        }
214 11
        foreach (['headers', 'cookies'] as $name) {
215 11
            if (isset($data[$name]) && is_array($data[$name])) {
216 10
                $response->{$name}->fromArray(array_merge($data[$name], $response->{$name}->toArray()));
217 10
            }
218 11
        }
219 11
        if (!empty($data['dynamicPlaceholders']) && is_array($data['dynamicPlaceholders'])) {
220 11
            if (empty($this->view->cacheStack)) {
221
                // outermost cache: replace placeholder with dynamic content
222 11
                $response->content = $this->updateDynamicContent($response->content, $data['dynamicPlaceholders']);
223 11
            }
224 11
            foreach ($data['dynamicPlaceholders'] as $name => $statements) {
225 11
                $this->view->addDynamicPlaceholder($name, $statements);
226 11
            }
227 11
        }
228 11
        $this->afterRestoreResponse(isset($data['cacheData']) ? $data['cacheData'] : null);
229 11
    }
230
231
    /**
232
     * Caches response properties.
233
     * @since 2.0.3
234
     */
235 12
    public function cacheResponse()
236
    {
237 12
        array_pop($this->view->cacheStack);
238 12
        $beforeCacheResponseResult = $this->beforeCacheResponse();
239 12
        if ($beforeCacheResponseResult === false) {
240
            $content = ob_get_clean();
241
            if (empty($this->view->cacheStack) && !empty($this->dynamicPlaceholders)) {
242
                $content = $this->updateDynamicContent($content, $this->dynamicPlaceholders);
243
            }
244
            echo $content;
245
            return;
246
        }
247
248 12
        $response = Yii::$app->getResponse();
249
        $data = [
250 12
            'cacheVersion' => 1,
251 12
            'cacheData' => is_array($beforeCacheResponseResult) ? $beforeCacheResponseResult : null,
252 12
            'content' => ob_get_clean()
253 12
        ];
254 12
        if ($data['content'] === false || $data['content'] === '') {
255 4
            return;
256
        }
257
258 12
        $data['dynamicPlaceholders'] = $this->dynamicPlaceholders;
259 12
        foreach (['format', 'version', 'statusCode', 'statusText'] as $name) {
260 12
            $data[$name] = $response->{$name};
261 12
        }
262 12
        $this->insertResponseCollectionIntoData($response, 'headers', $data);
263 12
        $this->insertResponseCollectionIntoData($response, 'cookies', $data);
264 12
        $this->cache->set($this->calculateCacheKey(), $data, $this->duration, $this->dependency);
265 12
        if (empty($this->view->cacheStack) && !empty($this->dynamicPlaceholders)) {
266 12
            $data['content'] = $this->updateDynamicContent($data['content'], $this->dynamicPlaceholders);
267 12
        }
268 12
        echo $data['content'];
269 12
    }
270
271
    /**
272
     * Inserts (or filters/ignores according to config) response headers/cookies into a cache data array.
273
     * @param Response $response the response.
274
     * @param string $collectionName currently it's `headers` or `cookies`.
275
     * @param array $data the cache data.
276
     */
277 12
    private function insertResponseCollectionIntoData(Response $response, $collectionName, array &$data)
278
    {
279 12
        $property = 'cache' . ucfirst($collectionName);
280 12
        if ($this->{$property} === false) {
281 9
            return;
282
        }
283
284 11
        $all = $response->{$collectionName}->toArray();
285 11
        if (is_array($this->{$property})) {
286 3
            $filtered = [];
287 3
            foreach ($this->{$property} as $name) {
288 3
                if ($collectionName === 'headers') {
289 2
                    $name = strtolower($name);
290 2
                }
291 3
                if (isset($all[$name])) {
292 3
                    $filtered[$name] = $all[$name];
293 3
                }
294 3
            }
295 3
            $all = $filtered;
296 3
        }
297 11
        $data[$collectionName] = $all;
298 11
    }
299
300
    /**
301
     * Replaces placeholders in content by results of evaluated dynamic statements.
302
     * @param string $content content to be parsed.
303
     * @param array $placeholders placeholders and their values.
304
     * @return string final content.
305
     * @since 2.0.11
306
     */
307 12
    protected function updateDynamicContent($content, $placeholders)
308
    {
309 12
        foreach ($placeholders as $name => $statements) {
310 12
            $placeholders[$name] = $this->view->evaluateDynamicContent($statements);
311 12
        }
312
313 12
        return strtr($content, $placeholders);
314
    }
315
316
    /**
317
     * @return array the key used to cache response properties.
318
     * @since 2.0.3
319
     */
320 12
    protected function calculateCacheKey()
321
    {
322 12
        $key = [__CLASS__];
323 12
        if ($this->varyByRoute) {
324 12
            $key[] = Yii::$app->requestedRoute;
325 12
        }
326 12
        if (is_array($this->variations)) {
327 1
            foreach ($this->variations as $value) {
328 1
                $key[] = $value;
329 1
            }
330 1
        }
331 12
        return $key;
332
    }
333
}
334