Completed
Push — remove-yii-autoloader ( 560d61...5f8600 )
by Alexander
27:04 queued 23:12
created

PageCache::insertResponseCollectionIntoData()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 23
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 15
nc 5
nop 3
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
    public function init()
140
    {
141
        parent::init();
142
        if ($this->view === null) {
143
            $this->view = Yii::$app->getView();
144
        }
145
    }
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
    public function beforeAction($action)
154
    {
155
        if (!$this->enabled) {
156
            return true;
157
        }
158
159
        $this->cache = Instance::ensure($this->cache, CacheInterface::class);
160
161
        if (is_array($this->dependency)) {
162
            $this->dependency = Yii::createObject($this->dependency);
163
        }
164
165
        $response = Yii::$app->getResponse();
166
        $data = $this->cache->get($this->calculateCacheKey());
167
        if (!is_array($data) || !isset($data['cacheVersion']) || $data['cacheVersion'] !== 1) {
168
            $this->view->cacheStack[] = $this;
169
            ob_start();
170
            ob_implicit_flush(false);
171
            $response->on(Response::EVENT_AFTER_SEND, [$this, 'cacheResponse']);
172
            Yii::debug('Valid page content is not found in the cache.', __METHOD__);
173
            return true;
174
        }
175
176
        $this->restoreResponse($response, $data);
177
        Yii::debug('Valid page content is found in the cache.', __METHOD__);
178
        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
    public function beforeCacheResponse()
189
    {
190
        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
    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
    }
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
    protected function restoreResponse($response, $data)
210
    {
211
        foreach (['format', 'protocolVersion', 'statusCode', 'reasonPhrase', 'content'] as $name) {
212
            $response->{$name} = $data[$name];
213
        }
214
215
        if (isset($data['headers'])) {
216
            $response->setHeaders($data['headers']);
217
        }
218
219
        if (isset($data['cookies']) && is_array($data['cookies'])) {
220
            $response->getCookies()->fromArray(array_merge($data['cookies'], $response->getCookies()->toArray()));
221
        }
222
223
        if (!empty($data['dynamicPlaceholders']) && is_array($data['dynamicPlaceholders'])) {
224
            if (empty($this->view->cacheStack)) {
225
                // outermost cache: replace placeholder with dynamic content
226
                $response->content = $this->updateDynamicContent($response->content, $data['dynamicPlaceholders']);
227
            }
228
            foreach ($data['dynamicPlaceholders'] as $name => $statements) {
229
                $this->view->addDynamicPlaceholder($name, $statements);
230
            }
231
        }
232
        $this->afterRestoreResponse(isset($data['cacheData']) ? $data['cacheData'] : null);
233
    }
234
235
    /**
236
     * Caches response properties.
237
     * @since 2.0.3
238
     */
239
    public function cacheResponse()
240
    {
241
        array_pop($this->view->cacheStack);
242
        $beforeCacheResponseResult = $this->beforeCacheResponse();
243
        if ($beforeCacheResponseResult === false) {
244
            $content = ob_get_clean();
245
            if (empty($this->view->cacheStack) && !empty($this->dynamicPlaceholders)) {
246
                $content = $this->updateDynamicContent($content, $this->dynamicPlaceholders);
247
            }
248
            echo $content;
249
            return;
250
        }
251
252
        $response = Yii::$app->getResponse();
253
        $data = [
254
            'cacheVersion' => 1,
255
            'cacheData' => is_array($beforeCacheResponseResult) ? $beforeCacheResponseResult : null,
256
            'content' => ob_get_clean(),
257
        ];
258
        if ($data['content'] === false || $data['content'] === '') {
259
            return;
260
        }
261
262
        $data['dynamicPlaceholders'] = $this->dynamicPlaceholders;
263
264
        $data = array_merge($data, [
265
            'format' => $response->format,
266
            'protocolVersion' => $response->getProtocolVersion(),
267
            'statusCode' => $response->getStatusCode(),
268
            'reasonPhrase' => $response->getReasonPhrase(),
269
        ]);
270
271
        $this->insertResponseCollectionIntoData($response, 'headers', $data);
272
        $this->insertResponseCollectionIntoData($response, 'cookies', $data);
273
        $this->cache->set($this->calculateCacheKey(), $data, $this->duration, $this->dependency);
274
        if (empty($this->view->cacheStack) && !empty($this->dynamicPlaceholders)) {
275
            $data['content'] = $this->updateDynamicContent($data['content'], $this->dynamicPlaceholders);
276
        }
277
        echo $data['content'];
278
    }
279
280
    /**
281
     * Inserts (or filters/ignores according to config) response headers/cookies into a cache data array.
282
     * @param Response $response the response.
283
     * @param string $collectionName currently it's `headers` or `cookies`.
284
     * @param array $data the cache data.
285
     */
286
    private function insertResponseCollectionIntoData(Response $response, $collectionName, array &$data)
287
    {
288
        $property = 'cache' . ucfirst($collectionName);
289
        if ($this->{$property} === false) {
290
            return;
291
        }
292
293
        $collection = $response->{$collectionName};
294
        $all = is_array($collection) ? $collection : $collection->toArray();
295
        if (is_array($this->{$property})) {
296
            $filtered = [];
297
            foreach ($this->{$property} as $name) {
298
                if ($collectionName === 'headers') {
299
                    $name = strtolower($name);
300
                }
301
                if (isset($all[$name])) {
302
                    $filtered[$name] = $all[$name];
303
                }
304
            }
305
            $all = $filtered;
306
        }
307
        $data[$collectionName] = $all;
308
    }
309
310
    /**
311
     * Replaces placeholders in content by results of evaluated dynamic statements.
312
     * @param string $content content to be parsed.
313
     * @param array $placeholders placeholders and their values.
314
     * @return string final content.
315
     * @since 2.0.11
316
     */
317
    protected function updateDynamicContent($content, $placeholders)
318
    {
319
        foreach ($placeholders as $name => $statements) {
320
            $placeholders[$name] = $this->view->evaluateDynamicContent($statements);
321
        }
322
323
        return strtr($content, $placeholders);
324
    }
325
326
    /**
327
     * @return array the key used to cache response properties.
328
     * @since 2.0.3
329
     */
330
    protected function calculateCacheKey()
331
    {
332
        $key = [__CLASS__];
333
        if ($this->varyByRoute) {
334
            $key[] = Yii::$app->requestedRoute;
335
        }
336
        if (is_array($this->variations)) {
337
            foreach ($this->variations as $value) {
338
                $key[] = $value;
339
            }
340
        }
341
        return $key;
342
    }
343
}
344