Passed
Push — master ( 6afa72...f96468 )
by M. Mikkel
04:08
created

CacheFlagService::implodeFlagsArray()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 8
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 16
rs 10
1
<?php
2
/**
3
 * Cache Flag plugin for Craft CMS 3.x
4
 *
5
 * Flag and clear template caches.
6
 *
7
 * @link      https://vaersaagod.no
8
 * @copyright Copyright (c) 2018 Mats Mikkel Rummelhoff
9
 */
10
11
namespace mmikkel\cacheflag\services;
12
13
use craft\base\Element;
14
use craft\elements\Asset;
15
use craft\elements\Category;
16
use craft\elements\Entry;
17
use craft\elements\GlobalSet;
18
use craft\elements\Tag;
19
use mmikkel\cacheflag\CacheFlag;
20
use mmikkel\cacheflag\events\BeforeDeleteFlaggedTemplateCachesEvent;
21
use mmikkel\cacheflag\events\AfterDeleteFlaggedTemplateCachesEvent;
22
use mmikkel\cacheflag\events\FlaggedTemplateCachesEvent;
23
use mmikkel\cacheflag\records\Flagged;
24
use mmikkel\cacheflag\records\Flags;
25
26
use Craft;
27
use craft\base\Component;
28
use craft\db\Query;
29
use craft\helpers\UrlHelper;
30
use yii\caching\TagDependency;
31
32
33
/**
34
 * @author    Mats Mikkel Rummelhoff
35
 * @package   CacheFlag
36
 * @since     1.0.0
37
 */
38
class CacheFlagService extends Component
39
{
40
41
    // Constants
42
    // =========================================================================
43
    /**
44
     * @event Event The event that is triggered before flagged template caches are deleted.
45
     */
46
    const EVENT_BEFORE_INVALIDATE_FLAGGED_CACHES = 'beforeInvalidateFlaggedCaches';
47
48
    /**
49
     * @event Event The event that is triggered after flagged template caches are invalidated.
50
     */
51
    const EVENT_AFTER_INVALIDATE_FLAGGED_CACHES = 'afterInvalidateFlaggedCaches';
52
53
    /**
54
     * @event Event The event that is triggered before flagged template caches are deleted.
55
     * @deprecated since 1.1.0. Use [[\mmikkel\cacheflag\services\CacheFlagService::EVENT_BEFORE_INVALIDATE_FLAGGED_CACHES]] instead.
56
     */
57
    const EVENT_BEFORE_DELETE_FLAGGED_CACHES = 'beforeDeleteFlaggedCaches';
58
59
    /**
60
     * @event Event The event that is triggered after flagged template caches are deleted.
61
     * @deprecated since 1.1.0. Use [[\mmikkel\cacheflag\services\CacheFlagService::EVENT_AFTER_INVALIDATE_FLAGGED_CACHES]] instead.
62
     */
63
    const EVENT_AFTER_DELETE_FLAGGED_CACHES = 'afterDeleteFlaggedCaches';
64
65
    // Public Methods
66
    // =========================================================================
67
68
    /**
69
     * @return array
70
     */
71
    public function getCpTabs(): array
72
    {
73
        return [
74
            'cacheFlagIndex' => array(
75
                'label' => Craft::t('cache-flag', 'Flags'),
76
                'url' => UrlHelper::url('cache-flag'),
77
            ),
78
            'about' => array(
79
                'label' => Craft::t('cache-flag', 'About'),
80
                'url' => UrlHelper::url('cache-flag/about'),
81
            ),
82
        ];
83
    }
84
85
    /**
86
     * @return array
87
     */
88
    public function getSources(): array
89
    {
90
        $sources = [
91
            'sections' => [
92
                'column' => 'sectionId',
93
                'name' => Craft::t('app', 'Sections'),
94
                'sources' => Craft::$app->getSections()->getAllSections(),
95
            ],
96
            'categoryGroups' => [
97
                'column' => 'categoryGroupId',
98
                'name' => Craft::t('app', 'Category Groups'),
99
                'sources' => Craft::$app->getCategories()->getAllGroups(),
100
            ],
101
            'volumes' => [
102
                'column' => 'volumeId',
103
                'name' => Craft::t('app', 'Asset Volumes'),
104
                'sources' => Craft::$app->getVolumes()->getAllVolumes(),
105
            ],
106
            'globalSets' => [
107
                'column' => 'globalSetId',
108
                'name' => Craft::t('app', 'Global Sets'),
109
                'sources' => Craft::$app->getGlobals()->getAllSets(),
110
            ],
111
            'elementTypes' => [
112
                'column' => 'elementType',
113
                'name' => Craft::t('app', 'Element Types'),
114
                'sources' => \array_map(function (string $elementType) {
115
                    return [
116
                        'id' => $elementType,
117
                        'name' => $elementType,
118
                    ];
119
                }, Craft::$app->getElements()->getAllElementTypes()),
120
            ],
121
        ];
122
123
        if (Craft::$app->getEdition() === 1) {
124
            $sources['userGroups'] = [
125
                'column' => 'userGroupId',
126
                'name' => Craft::t('app', 'User Groups'),
127
                'sources' => Craft::$app->getUserGroups()->getAllGroups(),
128
            ];
129
        }
130
131
        return $sources;
132
    }
133
134
    /**
135
     * @return array
136
     */
137
    public function getAllFlags(): array
138
    {
139
        return (new Query())
140
            ->select('*')
141
            ->from([Flags::tableName()])
142
            ->all();
143
    }
144
145
    /**
146
     * @param string|array $flags
147
     * @param string $sourceColumn
148
     * @param string $sourceId
149
     * @throws \Throwable
150
     * @throws \yii\db\Exception
151
     */
152
    public function saveFlags($flags, string $sourceColumn, string $sourceId)
153
    {
154
155
        if (!$flags) {
156
            return;
157
        }
158
159
        if (\is_array($flags)) {
160
            $flags = \implode(',', $flags);
161
        }
162
163
        $transaction = Craft::$app->getDb()->beginTransaction();
164
165
        try {
166
167
            $flagsId = (int)(new Query())
168
                ->select(['id'])
169
                ->from([Flags::tableName()])
170
                ->where([$sourceColumn => $sourceId])
171
                ->scalar();
172
173
            if ($flagsId) {
174
175
                Craft::$app->getDb()->createCommand()
176
                    ->update(
177
                        Flags::tableName(),
178
                        ['flags' => $flags],
179
                        "id=:id",
180
                        [':id' => $flagsId],
181
                        false
182
                    )
183
                    ->execute();
184
185
            } else {
186
187
                Craft::$app->getDb()->createCommand()
188
                    ->insert(
189
                        Flags::tableName(),
190
                        [
191
                            'flags' => $flags,
192
                            $sourceColumn => $sourceId,
193
                        ],
194
                        false)
195
                    ->execute();
196
            }
197
198
            $transaction->commit();
199
200
        } catch (\Throwable $e) {
201
            $transaction->rollBack();
202
203
            throw $e;
204
        }
205
    }
206
207
    /**
208
     * @param string $sourceColumn
209
     * @param string $sourceValue
210
     * @throws \Throwable
211
     * @throws \yii\db\Exception
212
     */
213
    public function deleteFlagsBySource(string $sourceColumn, string $sourceValue)
214
    {
215
216
        $transaction = Craft::$app->getDb()->beginTransaction();
217
218
        try {
219
220
            Craft::$app->getDb()->createCommand()
221
                ->delete(
222
                    Flags::tableName(),
223
                    [$sourceColumn => $sourceValue]
224
                )
225
                ->execute();
226
227
            $transaction->commit();
228
229
        } catch (\Throwable $e) {
230
            $transaction->rollBack();
231
232
            throw $e;
233
        }
234
    }
235
236
    /**
237
     * @return bool
238
     * @deprecated since 1.1.0
239
     */
240
    public function flagsHasCaches(): bool
241
    {
242
        return true;
243
    }
244
245
    /**
246
     * Invalidate all flagged template caches
247
     */
248
    public function invalidateAllFlaggedCaches()
249
    {
250
        TagDependency::invalidate(Craft::$app->getCache(), 'cacheflag');
251
    }
252
253
    /**
254
     * @return bool
255
     * @deprecated since 1.1.0. Use [[\mmikkel\cacheflag\services\CacheFlagService::invalidateAllFlaggedCaches()]] instead.
256
     */
257
    public function deleteAllFlaggedCaches(): bool
258
    {
259
        Craft::$app->getDeprecator()->log('CacheFlagService::deleteAllFlaggedCaches()', 'deleteAllFlaggedCaches() has been deprecated. Use \mmikkel\cacheflag\services\CacheFlagService::invalidateAllFlaggedCaches() instead.');
260
        $this->invalidateAllFlaggedCaches();
261
        return true;
262
    }
263
264
    /**
265
     * @param Element $element
266
     * @return bool
267
     */
268
    public function invalidateFlaggedCachesByElement(Element $element): bool
269
    {
270
        // Collect all flags for this element
271
        $query = (new Query())
272
            ->select(['flags'])
273
            ->from(Flags::tableName());
274
275
        $elementType = \get_class($element);
276
        $dynamicFlags = ["element:$element->id", "element:$element->uid"];
277
278
        switch ($elementType) {
279
            case 'craft\elements\Asset':
280
                /** @var Asset $element */
281
                $query->orWhere([
282
                    'volumeId' => $element->volumeId,
283
                ]);
284
                $dynamicFlags[] = "asset:$element->id";
285
                $dynamicFlags[] = "asset:$element->uid";
286
                break;
287
            case 'craft\elements\Category':
288
                /** @var Category $element */
289
                $query->orWhere([
290
                    'categoryGroupId' => $element->groupId,
291
                ]);
292
                $dynamicFlags[] = "category:$element->id";
293
                $dynamicFlags[] = "category:$element->uid";
294
                break;
295
            case 'craft\elements\Entry':
296
                /** @var Entry $element */
297
                $query->orWhere([
298
                    'sectionId' => $element->sectionId,
299
                ]);
300
                $dynamicFlags[] = "entry:$element->id";
301
                $dynamicFlags[] = "entry:$element->uid";
302
                break;
303
            case 'craft\elements\GlobalSet':
304
                /** @var GlobalSet $element */
305
                $query->orWhere([
306
                    'globalSetId' => $element->id,
307
                ]);
308
                $dynamicFlags[] = "globalSet:$element->id";
309
                $dynamicFlags[] = "globalSet:$element->uid";
310
                break;
311
            case 'craft\elements\Tag':
312
                /** @var Tag $element */
313
                $query->orWhere([
314
                    'tagGroupId' => $element->groupId,
315
                ]);
316
                $dynamicFlags[] = "tag:$element->id";
317
                $dynamicFlags[] = "tag:$element->uid";
318
                break;
319
            case 'craft\elements\User':
320
                /** @var User $element */
321
                foreach ($element->getGroups() as $userGroup) {
322
                    $query->orWhere([
323
                        'userGroupId' => $userGroup->id,
324
                    ]);
325
                }
326
                $dynamicFlags[] = "user:$element->id";
327
                $dynamicFlags[] = "user:$element->uid";
328
                break;
329
        }
330
331
        $query->orWhere([
332
            'elementType' => $elementType,
333
        ]);
334
335
        $flags = \array_unique(\array_merge($query->column(), $dynamicFlags));
336
337
        return $this->invalidateFlaggedCachesByFlags($flags);
338
    }
339
340
    /**
341
     * @param Element $element
342
     * @return bool
343
     * @deprecated since 1.1.0. Use [[\mmikkel\cacheflag\services\CacheFlagService::invalidateFlaggedCachesByElement()]] instead.
344
     */
345
    public function deleteFlaggedCachesByElement(Element $element): bool
346
    {
347
        Craft::$app->getDeprecator()->log('CacheFlagService::deleteFlaggedCachesByElement()', 'deleteFlaggedCachesByElement() has been deprecated. Use \mmikkel\cacheflag\services\CacheFlagService::invalidateFlaggedCachesByElement() instead.');
348
        return $this->invalidateFlaggedCachesByElement($element);
349
    }
350
351
    /**
352
     * @param string|string[]|null $flags
353
     * @return bool
354
     */
355
    public function invalidateFlaggedCachesByFlags($flags): bool
356
    {
357
358
        if (!$flags) {
359
            return false;
360
        }
361
362
        if (\is_array($flags)) {
363
            $flags = $this->implodeFlagsArray($flags);
364
        } else {
365
            $flags = \preg_replace('/\s+/', '', $flags);
366
        }
367
368
        $flags = \array_values(\array_unique(\explode(',', $flags)));
369
370
        if (empty($flags)) {
371
            return false;
372
        }
373
374
        // Fire a 'beforeDeleteFlaggedCaches' event
375
        if ($this->hasEventHandlers(/** @scrutinizer ignore-deprecated */ self::EVENT_BEFORE_DELETE_FLAGGED_CACHES)) {
376
            Craft::$app->getDeprecator()->log('CacheFlagService::EVENT_BEFORE_DELETE_FLAGGED_CACHES', 'The `EVENT_BEFORE_DELETE_FLAGGED_CACHES` event has been deprecated. Use \mmikkel\cacheflag\services\CacheFlagService::EVENT_BEFORE_INVALIDATE_FLAGGED_CACHES instead.');
377
            $this->trigger(/** @scrutinizer ignore-deprecated */ self::EVENT_BEFORE_DELETE_FLAGGED_CACHES, /** @scrutinizer ignore-deprecated */ new BeforeDeleteFlaggedTemplateCachesEvent([
378
                'cacheIds' => [],
379
                'flags' => $flags,
380
            ]));
381
        }
382
383
        // Fire a `beforeInvalidateFlaggedCaches` event
384
        if ($this->hasEventHandlers(self::EVENT_BEFORE_INVALIDATE_FLAGGED_CACHES)) {
385
            $this->trigger(self::EVENT_BEFORE_INVALIDATE_FLAGGED_CACHES, new FlaggedTemplateCachesEvent([
386
                'flags' => $flags,
387
            ]));
388
        }
389
390
        $flagTags = \array_map(function (string $flag) {
391
            return "cacheflag::$flag";
392
        }, $flags);
393
394
        TagDependency::invalidate(Craft::$app->getCache(), $flagTags);
395
396
        // Fire a 'afterDeleteFlaggedCaches' event
397
        if ($this->hasEventHandlers(/** @scrutinizer ignore-deprecated */ self::EVENT_AFTER_DELETE_FLAGGED_CACHES)) {
398
            Craft::$app->getDeprecator()->log('CacheFlagService::EVENT_AFTER_DELETE_FLAGGED_CACHES', 'The `EVENT_AFTER_DELETE_FLAGGED_CACHES` event has been deprecated. Use \mmikkel\cacheflag\services\CacheFlagService::EVENT_AFTER_INVALIDATE_FLAGGED_CACHES instead.');
399
            $this->trigger(/** @scrutinizer ignore-deprecated */ self::EVENT_AFTER_DELETE_FLAGGED_CACHES, /** @scrutinizer ignore-deprecated */ new AfterDeleteFlaggedTemplateCachesEvent([
400
                'cacheIds' => [],
401
                'flags' => $flags,
402
            ]));
403
        }
404
405
        // Fire a 'afterInvalidateFlaggedCaches' event
406
        if ($this->hasEventHandlers(self::EVENT_AFTER_INVALIDATE_FLAGGED_CACHES)) {
407
            $this->trigger(self::EVENT_AFTER_INVALIDATE_FLAGGED_CACHES, new FlaggedTemplateCachesEvent([
408
                'flags' => $flags,
409
            ]));
410
        }
411
412
        return true;
413
    }
414
415
    /**
416
     * @param string|string[] $flags
417
     * @return bool
418
     * @deprecated since 1.1.0. Use [[\mmikkel\cacheflag\services\CacheFlagService::invalidateFlaggedCachesByFlags()]] instead.
419
     */
420
    public function deleteFlaggedCachesByFlags($flags): bool
421
    {
422
        Craft::$app->getDeprecator()->log('CacheFlagService::deleteFlaggedCachesByFlags()', 'deleteFlaggedCachesByFlags() has been deprecated. Use \mmikkel\cacheflag\services\CacheFlagService::invalidateFlaggedCachesByFlags() instead.');
423
        return $this->invalidateFlaggedCachesByFlags($flags);
424
    }
425
426
    /*
427
     * Protected methods
428
     */
429
    /**
430
     * @param array $flagsArray
431
     * @return string
432
     */
433
    protected function implodeFlagsArray(array $flagsArray): string
434
    {
435
436
        $flags = '';
437
438
        foreach ($flagsArray as $item) {
439
            if (\is_array($item)) {
440
                $flags .= "{$this->implodeFlagsArray($item)},";
441
            } else {
442
                $flags .= \preg_replace('/\s+/', '', $item) . ',';
443
            }
444
        }
445
446
        $flags = \substr($flags, 0, 0 - strlen(','));
447
448
        return $flags;
449
    }
450
}
451