update_cache_key()   B
last analyzed

Complexity

Conditions 10
Paths 15

Size

Total Lines 40
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
cc 10
eloc 26
c 7
b 0
f 0
nc 15
nop 1
dl 0
loc 40
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Sunnysideup\SimpleTemplateCaching\Extensions;
4
5
use Exception;
6
use Page;
0 ignored issues
show
Bug introduced by
The type Page was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use SilverStripe\CMS\Model\SiteTree;
8
use SilverStripe\Control\Controller;
9
use SilverStripe\Control\Director;
10
use SilverStripe\Core\Injector\Injector;
11
use SilverStripe\Forms\CheckboxField;
12
use SilverStripe\Forms\FieldList;
13
use SilverStripe\Forms\GridField\GridField;
14
use SilverStripe\Forms\GridField\GridFieldConfig_RecordViewer;
15
use SilverStripe\Forms\NumericField;
16
use SilverStripe\Forms\ReadonlyField;
17
use SilverStripe\Core\Extension;
18
use SilverStripe\Forms\HeaderField;
19
use SilverStripe\ORM\DB;
20
use SilverStripe\ORM\FieldType\DBDatetime;
21
use SilverStripe\SiteConfig\SiteConfig;
22
use Sunnysideup\SimpleTemplateCaching\Model\ObjectsUpdated;
23
24
/**
25
 * Class \Sunnysideup\SimpleTemplateCaching\Extensions\SimpleTemplateCachingSiteConfigExtension.
26
 *
27
 * @property SiteConfig|SimpleTemplateCachingSiteConfigExtension $owner
28
 * @property bool $HasCaching
29
 * @property bool $HasPartialCaching
30
 * @property bool $HasResourceCaching
31
 * @property int $PublicCacheDurationInSeconds
32
 * @property bool $RecordCacheUpdates
33
 * @property string $CacheKeyLastEdited
34
 * @property string $ClassNameLastEdited
35
 * @property int $ResourceCachingTimeInSeconds
36
 */
37
class SimpleTemplateCachingSiteConfigExtension extends Extension
38
{
39
    private const MAX_OBJECTS_UPDATED = 1000;
40
41
    private static string $image_cache_directive = '
42
<IfModule mod_headers.c>
43
  <FilesMatch "^(?:_resources/themes|assets)/.*\.(jpg|jpeg|png|gif|webp|svg|avif)$">
44
    Header set Cache-Control "public, max-age=600"
45
  </FilesMatch>
46
</IfModule>
47
    ';
48
49
    private static string $css_and_js_cache_directive = '
50
<IfModule mod_headers.c>
51
  <FilesMatch "^_resources/themes/.*\.(js|css)$">
52
    Header set Cache-Control "public, max-age=600"
53
  </FilesMatch>
54
</IfModule>
55
    ';
56
57
    private static string $font_cache_directive = '
58
<IfModule mod_headers.c>
59
    <FilesMatch "^_resources/themes/.*\.(woff|woff2|ttf|otf|eot)$">
60
    Header set Cache-Control "public, max-age=600"
61
    </FilesMatch>
62
</IfModule>
63
    ';
64
65
    private static $db = [
66
        'HasCaching' => 'Boolean(1)',
67
        'HasPartialCaching' => 'Boolean(1)',
68
        'HasResourceCaching' => 'Boolean(1)',
69
        'PublicCacheDurationInSeconds' => 'Int',
70
        'RecordCacheUpdates' => 'Boolean(0)',
71
        'CacheKeyLastEdited' => 'DBDatetime',
72
        'ClassNameLastEdited' => 'Varchar(200)',
73
        'ResourceCachingTimeInSeconds' => 'Int',
74
    ];
75
76
    public function updateCMSFields(FieldList $fields)
77
    {
78
        $owner = $this->getOwner();
79
        $name = '[none]';
80
        if (class_exists((string) $owner->ClassNameLastEdited)) {
81
            $name = Injector::inst()->get($owner->ClassNameLastEdited)->i18n_singular_name();
82
        }
83
84
        // page caching
85
        $fields->addFieldsToTab(
86
            'Root.Caching',
87
            [
88
                HeaderField::create('FullPageCachingHeader', 'Full Page Caching'),
89
                CheckboxField::create('HasCaching', 'Allow caching of entire pages?')
90
                    ->setDescription(
91
                        'You will also need to set up the cache time below for it to be enabled.
92
                        You can set a default time below, but you can also set the time for individual pages.'
93
                    ),
94
            ]
95
        );
96
        if ($owner->HasCaching) {
97
            $fields->addFieldsToTab(
98
                'Root.Caching',
99
                [
100
                    NumericField::create('PublicCacheDurationInSeconds', 'Cache time for ALL pages')
101
                        ->setDescription(
102
                            'USE WITH CARE - This will apply caching to ALL pages on the site.
103
                            Time is in seconds (e.g. 600 = 10 minutes).
104
                            Cache time on individual pages will override this value set here.
105
                            The total number of pages on the site with an individual caching time is: ' . Page::get()->filter('PublicCacheDurationInSeconds:GreaterThan', 0)->count()
106
                        ),
107
                ]
108
            );
109
        }
110
111
        //partial caching
112
        $fields->addFieldsToTab(
113
            'Root.Caching',
114
            [
115
                HeaderField::create('PartialCachingHeader', 'Partial Caching'),
116
                CheckboxField::create('HasPartialCaching', 'Allow partial template caching?')
117
                    ->setDescription(
118
                        'This should usually be turned on unless you want to make sure no templates are cached in any part at all.'
119
                    ),
120
            ]
121
        );
122
        if ($owner->HasPartialCaching) {
123
            $fields->addFieldsToTab(
124
                'Root.Caching',
125
                [
126
                    CheckboxField::create('RecordCacheUpdates', 'Keep a record of what is being changed?')
127
                        ->setDescription(
128
                            'To work out when the cache is being cleared,
129
                            you can keep a record of the last ' . self::MAX_OBJECTS_UPDATED . ' records changed.
130
                            Only turn this on temporarily for tuning purposes.'
131
                        ),
132
                ]
133
            );
134
            if ($this->getOwner()->RecordCacheUpdates) {
135
                $fields->addFieldsToTab(
136
                    'Root.Caching',
137
                    [
138
139
                        ReadonlyField::create('CacheKeyLastEditedNice', 'Last database change', $owner->dbObject('CacheKeyLastEdited')->ago())
140
                            ->setDescription(
141
                                'The frontend template cache will be invalidated every time this value changes.
142
                                                The value changes every time anything is changed in the database.'
143
                            ),
144
                        ReadonlyField::create('ClassNameLastEditedNice', 'Last record updated', $name)
145
                            ->setDescription('The last record to invalidate the cache.'),
146
147
                        GridField::create(
148
                            'ObjectsUpdated',
149
                            'Last ' . self::MAX_OBJECTS_UPDATED . ' records updated',
150
                            ObjectsUpdated::get()->limit(self::MAX_OBJECTS_UPDATED),
151
                            GridFieldConfig_RecordViewer::create()
152
                        )
153
                            ->setDescription(
154
                                '
155
                                This is a list of the last ' . self::MAX_OBJECTS_UPDATED . ' records updated.
156
                                It is used to track changes to the database.
157
                                It includes: ' . ObjectsUpdated::classes_edited()
158
                            ),
159
                    ]
160
                );
161
            }
162
        }
163
        // resource caching
164
        $fields->addFieldsToTab(
165
            'Root.Caching',
166
            [
167
                HeaderField::create('ResourceCachingHeader', 'Resource Caching'),
168
                CheckboxField::create('HasResourceCaching', 'Allow caching of resources (e.g. images, styles, etc.). ')
169
                    ->setDescription(
170
                        'This will add cache control headers to your .htaccess file for images, styles, and scripts.
171
                        This will help with performance, but once cached, a cache can not be cleared without changing the file name.'
172
                    ),
173
                NumericField::create('ResourceCachingTimeInSeconds', 'Cache time for resources')
174
                    ->setDescription(
175
                        'Time is in seconds (e.g. 600 = 10 minutes).
176
                        This will be used for all resources on the site (fonts, images, styles, and scripts).'
177
                    ),
178
            ]
179
        );
180
    }
181
182
    public static function site_cache_key(): string
183
    {
184
        $obj = SiteConfig::current_site_config();
185
        if ($obj->HasPartialCaching) {
186
            return 'ts_' . strtotime((string) $obj->CacheKeyLastEdited);
187
        }
188
189
        return 'ts_' . time();
190
    }
191
192
    public static function update_cache_key(?string $className = '')
193
    {
194
        // important - avoid endless loop!
195
        if (SiteConfig::get()->exists()) {
196
            $howOldIsIt = DB::query('SELECT Created FROM SiteConfig LIMIT 1')->value();
197
            if ($howOldIsIt && strtotime((string) $howOldIsIt) > strtotime('-5 minutes')) {
198
                return;
199
            }
200
        } else {
201
            return;
202
        }
203
        $obj = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $obj is dead and can be removed.
Loading history...
204
        try {
205
            $obj = SiteConfig::current_site_config();
206
            if ($obj->HasPartialCaching) {
207
                DB::query('
208
                    UPDATE "SiteConfig"
209
                    SET
210
                        "CacheKeyLastEdited" = \'' . DBDatetime::now()->Rfc2822() . '\',
211
                        "ClassNameLastEdited" = \'' . addslashes((string) $className) . '\'
212
                    WHERE ID = ' . $obj->ID . '
213
                    LIMIT 1
214
                ;');
215
                if ($obj->RecordCacheUpdates) {
216
                    $recordId = Injector::inst()
217
                        ->create(ObjectsUpdated::class, ['ClassNameLastEdited' => $className])
218
                        ->write();
219
                    DB::query('DELETE FROM "ObjectsUpdated" WHERE "ID" < ' . (int) ($recordId - self::MAX_OBJECTS_UPDATED));
220
                }
221
            } else {
222
                DB::query('TRUNCATE "ObjectsUpdated";');
223
            }
224
        } catch (Exception $e) {
225
            if (isset($obj) && $obj && $obj->ID) {
226
                DB::query('
227
                    UPDATE "SiteConfig"
228
                    SET
229
                        "CacheKeyLastEdited" = \'' . DBDatetime::now()->Rfc2822() . '\',
230
                        "ClassNameLastEdited" = \'ERROR\'
231
                    WHERE ID = ' . $obj->ID . '
232
                    LIMIT 1
233
                ;');
234
            }
235
        }
236
    }
237
238
    public function onAfterWrite()
239
    {
240
        $this->updateHtaccess();
241
    }
242
243
    public function requireDefaultRecords()
244
    {
245
        $this->updateHtaccess();
246
    }
247
248
    protected function updateHtaccess()
249
    {
250
        $owner = $this->getOwner();
251
        $currentSiteConfig = SiteConfig::current_site_config();
252
        if ((int) SiteConfig::get()->count() > 100) {
253
            if ($currentSiteConfig) {
0 ignored issues
show
introduced by
$currentSiteConfig is of type SilverStripe\SiteConfig\SiteConfig, thus it always evaluated to true.
Loading history...
254
                DB::alteration_message('Deleting all SiteConfig records except for the current one.', 'deleted');
255
                DB::query('DELETE FROM "SiteConfig" WHERE ID <> ' . $currentSiteConfig->ID);
256
            }
257
        }
258
        foreach (
259
            [
260
                'IMAGE_CACHE_DIRECTIVE' => $currentSiteConfig->config()->get('image_cache_directive'),
261
                'CSS_JS_CACHE_DIRECTIVE' => $currentSiteConfig->config()->get('css_and_js_cache_directive'),
262
                'FONT_CACHE_DIRECTIVE' => $currentSiteConfig->config()->get('font_cache_directive'),
263
            ] as $key => $value
264
        ) {
265
            if (! $currentSiteConfig->HasResourceCaching) {
266
                $value = '';
267
            }
268
            if ($owner->ResourceCachingTimeInSeconds) {
269
                $value = str_replace('max-age=600', 'max-age=' . $owner->ResourceCachingTimeInSeconds, $value);
270
            }
271
272
            $this->updateHtaccessForOne($key, $value);
273
        }
274
    }
275
276
    public function DoesNotHaveCaching(): bool
277
    {
278
        $owner = $this->getOwner();
279
        return ! $owner->HasCaching;
280
    }
281
282
    protected function updateHtaccessForOne(string $code, string $toAdd)
283
    {
284
        $htaccessPath = Controller::join_links(Director::publicFolder(), '.htaccess');
285
        $htaccessContent = file_get_contents($htaccessPath);
286
        $originalContent = $htaccessContent;
287
288
        // Define start and end comments
289
        $startComment = PHP_EOL . "# auto add start " . $code . PHP_EOL;
290
        $endComment = PHP_EOL . "# auto add end " . $code . PHP_EOL;
291
292
        // Full content to replace or add
293
        $toAddFull = $startComment . $toAdd . $endComment;
294
295
        // Check if the section already exists
296
        $pattern = "/" . preg_quote($startComment, '/') . ".*?" . preg_quote($endComment, '/') . "/s";
297
        if (preg_match($pattern, $htaccessContent)) {
298
            // Replace existing content between the start and end comments
299
            $htaccessContent = preg_replace($pattern, $toAddFull, $htaccessContent);
300
        } else {
301
            // Prepend the new content if not found
302
            $htaccessContent = $toAddFull . $htaccessContent;
303
        }
304
        if ($originalContent !== $htaccessContent) {
305
            // Save the updated .htaccess file
306
            DB::alteration_message('Updating .htaccess file with ' . $code . ' cache directive', 'created');
307
            file_put_contents($htaccessPath, $htaccessContent);
308
        }
309
    }
310
}
311