TemplateCachesService   A
last analyzed

Complexity

Total Complexity 30

Size/Duplication

Total Lines 221
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 30
eloc 80
c 3
b 0
f 0
dl 0
loc 221
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A _cacheKey() 0 9 2
A endTemplateCache() 0 35 5
A startTemplateCache() 0 9 3
A _path() 0 22 5
A _getTagsForFlags() 0 18 3
A _isTemplateCachingEnabled() 0 17 6
A getTemplateCache() 0 31 6
1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: mmikkel
5
 * Date: 14/07/2018
6
 * Time: 11:55
7
 */
8
9
namespace mmikkel\cacheflag\services;
10
11
use Craft;
12
use craft\base\Component;
13
use craft\helpers\StringHelper;
14
use yii\caching\TagDependency;
15
16
use DateTime;
17
18
/**
19
 * Class TemplateCachesService
20
 * @author    Mats Mikkel Rummelhoff
21
 * @package mmikkel\cacheflag\services
22
 * @since 1.0.0
23
 */
24
class TemplateCachesService extends Component
25
{
26
27
    /** @var bool */
28
    private $_collectElementTags = false;
29
30
    /** @var bool */
31
    private bool $_enabled;
32
33
    /** @var bool */
34
    private bool $_enabledGlobally;
35
36
    /**
37
     * @var string|null The current request's path
38
     * @see _path()
39
     */
40
    private $_path;
41
42
    // Public Methods
43
    // =========================================================================
44
    /**
45
     * Returns a cached template by its key.
46
     *
47
     * @param string $key The template cache key
48
     * @param string|string[]|null $flags The Cache Flag flags this cache would've been flagged with
49
     * @param bool $collectElementTags Whether to cache element queries or not
50
     * @param bool $global Whether the cache would have been stored globally.
51
     * @return string|null
52
     */
53
    public function getTemplateCache(string $key, $flags, bool $collectElementTags, bool $global)
54
    {
55
        // Make sure template caching is enabled
56
        if ($this->_isTemplateCachingEnabled($global) === false) {
57
            return null;
58
        }
59
60
        $this->_collectElementTags = $collectElementTags;
61
62
        $cacheKey = $this->_cacheKey($key, $global);
63
        $data = Craft::$app->getCache()->get($cacheKey);
64
65
        if ($data === false) {
66
            return null;
67
        }
68
69
        list($body, $tags) = $data;
70
71
        // Make sure the cache was tagged w/ the same flags
72
        $flagTags = $this->_getTagsForFlags($flags);
73
        $cachedFlagTags = \array_filter($tags, function (string $tag) {
74
            return \strpos($tag, 'cacheflag') === 0 || $tag === 'element' || $tag === 'template';
75
        });
76
77
        if (\array_diff($flagTags, $cachedFlagTags) != \array_diff($cachedFlagTags, $flagTags)) {
78
            return null;
79
        }
80
81
        // If we're actively collecting element cache tags, add this cache's tags to the collection
82
        Craft::$app->getElements()->collectCacheTags($tags);
83
        return $body;
84
    }
85
86
    /**
87
     *
88
     */
89
    public function startTemplateCache(bool $global = false)
90
    {
91
        // Make sure template caching is enabled
92
        if ($this->_isTemplateCachingEnabled($global) === false) {
93
            return;
94
        }
95
96
        if ($this->_collectElementTags) {
97
            Craft::$app->getElements()->startCollectingCacheTags();
98
        }
99
    }
100
101
    /**
102
     * Ends a template cache.
103
     *
104
     * @param string $key The template cache key.
105
     * @param string|null $flags The flags this cache should be flagged with.
106
     * @param bool $global Whether the cache should be stored globally.
107
     * @param string|null $duration How long the cache should be stored for. Should be a [relative time format](http://php.net/manual/en/datetime.formats.relative.php).
108
     * @param mixed|null $expiration When the cache should expire.
109
     * @param string $body The contents of the cache.
110
     * @throws \Throwable
111
     */
112
    public function endTemplateCache(string $key, $flags, bool $global, string $duration = null, /** @scrutinizer ignore-unused */ $expiration, string $body)
113
    {
114
115
        // Make sure template caching is enabled
116
        if ($this->_isTemplateCachingEnabled($global) === false) {
117
            return;
118
        }
119
120
        // If there are any transform generation URLs in the body, don't cache it.
121
        // stripslashes($body) in case the URL has been JS-encoded or something.
122
        if (StringHelper::contains(stripslashes($body), 'assets/generate-transform')) {
123
            return;
124
        }
125
126
        // Get flag tags
127
        $flagTags = $this->_getTagsForFlags($flags);
128
129
        if ($this->_collectElementTags) {
130
            // If we're collecting element tags, collect the flag tags too, and end the collection
131
            Craft::$app->getElements()->collectCacheTags($flagTags);
132
            $dep = Craft::$app->getElements()->stopCollectingCacheTags();
133
        } else {
134
            // If not, just tag it with the flags
135
            $dep = new TagDependency([
136
                'tags' => $flagTags,
137
            ]);
138
        }
139
140
        $cacheKey = $this->_cacheKey($key, $global);
141
142
        if ($duration !== null) {
143
            $duration = (new DateTime($duration))->getTimestamp() - time();
144
        }
145
146
        Craft::$app->getCache()->set($cacheKey, [$body, $dep->tags], $duration, $dep);
147
    }
148
149
    // Private Methods
150
    // =========================================================================
151
    /**
152
     * Returns whether template caching is enabled, based on the 'enableTemplateCaching' config setting.
153
     *
154
     * @return bool Whether template caching is enabled
155
     */
156
    private function _isTemplateCachingEnabled(bool $global): bool
157
    {
158
        if (!isset($this->_enabled)) {
159
            if (!Craft::$app->getConfig()->getGeneral()->enableTemplateCaching) {
160
                $this->_enabled = $this->_enabledGlobally = false;
161
            } else {
162
                // Don't enable template caches for Live Preview/tokenized requests
163
                $request = Craft::$app->getRequest();
164
                if ($request->getIsPreview() || $request->getHadToken()) {
165
                    $this->_enabled = $this->_enabledGlobally = false;
166
                } else {
167
                    $this->_enabled = !$request->getIsConsoleRequest();
168
                    $this->_enabledGlobally = true;
169
                }
170
            }
171
        }
172
        return $global ? $this->_enabledGlobally : $this->_enabled;
173
    }
174
175
    /**
176
     * Defines a data cache key that should be used for a template cache.
177
     *
178
     * @param string $key
179
     * @param bool $global
180
     */
181
    private function _cacheKey(string $key, bool $global): string
182
    {
183
        $cacheKey = "template::$key::" . Craft::$app->getSites()->getCurrentSite()->id;
184
185
        if (!$global) {
186
            $cacheKey .= '::' . $this->_path();
187
        }
188
189
        return $cacheKey;
190
    }
191
192
    /**
193
     * Returns the current request path, including a "site:" or "cp:" prefix.
194
     *
195
     * @return string
196
     * @throws \yii\base\InvalidConfigException
197
     */
198
    private function _path(): string
199
    {
200
        if ($this->_path !== null) {
201
            return $this->_path;
202
        }
203
204
        if (Craft::$app->getRequest()->getIsCpRequest()) {
205
            $this->_path = 'cp:';
206
        } else {
207
            $this->_path = 'site:';
208
        }
209
210
        $this->_path .= Craft::$app->getRequest()->getPathInfo();
211
        if (Craft::$app->getDb()->getIsMysql()) {
212
            $this->_path = StringHelper::encodeMb4($this->_path);
213
        }
214
215
        if (($pageNum = Craft::$app->getRequest()->getPageNum()) != 1) {
216
            $this->_path .= '/' . Craft::$app->getConfig()->getGeneral()->getPageTrigger() . $pageNum;
217
        }
218
219
        return $this->_path;
220
    }
221
222
    /**
223
     * @param string|array|null $flags
224
     * @param string $delimiter
225
     * @return array
226
     */
227
    private function _getTagsForFlags($flags, string $delimiter = '|'): array
228
    {
229
        $tagsArray = ['template', 'cacheflag'];
230
        if (\is_array($flags)) {
231
            $flags = \implode(',', \array_map(function ($flag) {
232
                return \preg_replace('/\s+/', '', $flag);
233
            }, $flags));
234
        } else {
235
            $flags = \preg_replace('/\s+/', '', $flags);
236
        }
237
        $flags = \array_filter(\explode($delimiter, $flags));
238
        $tagsArray = \array_merge($tagsArray, \array_map(function (string $flag) {
239
            return "cacheflag::$flag";
240
        }, $flags));
241
        if ($this->_collectElementTags) {
242
            $tagsArray[] = 'element';
243
        }
244
        return \array_unique(($tagsArray));
245
    }
246
247
}
248