Passed
Push — master ( 4c4acc...0b4de6 )
by M. Mikkel
04:50
created

CacheFlagService::implodeFlagsArray()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 1
dl 0
loc 16
rs 10
c 0
b 0
f 0
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 mmikkel\cacheflag\CacheFlag;
15
use mmikkel\cacheflag\events\BeforeDeleteFlaggedTemplateCachesEvent;
16
use mmikkel\cacheflag\events\AfterDeleteFlaggedTemplateCachesEvent;
17
use mmikkel\cacheflag\records\Flagged;
18
use mmikkel\cacheflag\records\Flags;
19
20
use Craft;
21
use craft\base\Component;
22
use craft\db\Query;
23
use craft\helpers\UrlHelper;
24
25
26
/**
27
 * @author    Mats Mikkel Rummelhoff
28
 * @package   CacheFlag
29
 * @since     1.0.0
30
 */
31
class CacheFlagService extends Component
32
{
33
34
    // Constants
35
    // =========================================================================
36
    /**
37
     * @event Event The event that is triggered before flagged template caches are deleted.
38
     */
39
    const EVENT_BEFORE_DELETE_FLAGGED_CACHES = 'beforeDeleteFlaggedCaches';
40
41
    /**
42
     * @event Event The event that is triggered after flagged template caches are deleted.
43
     */
44
    const EVENT_AFTER_DELETE_FLAGGED_CACHES = 'afterDeleteFlaggedCaches';
45
46
    // Public Methods
47
    // =========================================================================
48
49
    /**
50
     * @return array
51
     */
52
    public function getCpTabs(): array
53
    {
54
        return [
55
            'cacheFlagIndex' => array(
56
                'label' => '',
57
                'url' => UrlHelper::url('cache-flag'),
58
            ),
59
            'about' => array(
60
                'label' => Craft::t('cache-flag', 'About Cache Flag'),
61
                'url' => UrlHelper::url('cache-flag/about'),
62
            ),
63
        ];
64
    }
65
66
    /**
67
     * @return array
68
     */
69
    public function getSources(): array
70
    {
71
        $sources = [
72
            'sections' => [
73
                'column' => 'sectionId',
74
                'name' => Craft::t('app', 'Sections'),
75
                'sources' => Craft::$app->getSections()->getAllSections(),
76
            ],
77
            'categoryGroups' => [
78
                'column' => 'categoryGroupId',
79
                'name' => Craft::t('app', 'Category Groups'),
80
                'sources' => Craft::$app->getCategories()->getAllGroups(),
81
            ],
82
            'volumes' => [
83
                'column' => 'volumeId',
84
                'name' => Craft::t('app', 'Asset Volumes'),
85
                'sources' => Craft::$app->getVolumes()->getAllVolumes(),
86
            ],
87
            'globalSets' => [
88
                'column' => 'globalSetId',
89
                'name' => Craft::t('app', 'Global Sets'),
90
                'sources' => Craft::$app->getGlobals()->getAllSets(),
91
            ],
92
            'elementTypes' => [
93
                'column' => 'elementType',
94
                'name' => Craft::t('app', 'Element Types'),
95
                'sources' => \array_map(function (string $elementType) {
96
                    return [
97
                        'id' => $elementType,
98
                        'name' => $elementType,
99
                    ];
100
                }, Craft::$app->getElements()->getAllElementTypes()),
101
            ],
102
        ];
103
104
        if (Craft::$app->getEdition() === 1) {
105
            $sources['userGroups'] = [
106
                'column' => 'userGroupId',
107
                'name' => Craft::t('app', 'User Groups'),
108
                'sources' => Craft::$app->getUserGroups()->getAllGroups(),
109
            ];
110
        }
111
112
        return $sources;
113
    }
114
115
    /**
116
     * @return array
117
     */
118
    public function getAllFlags(): array
119
    {
120
        return (new Query())
121
            ->select('*')
122
            ->from([Flags::tableName()])
123
            ->all();
124
    }
125
126
    /**
127
     * @param $flags
128
     * @param string $sourceColumn
129
     * @param string $sourceId
130
     * @throws \Throwable
131
     * @throws \yii\db\Exception
132
     */
133
    public function saveFlags($flags, string $sourceColumn, string $sourceId)
134
    {
135
136
        if (!$flags) {
137
            return;
138
        }
139
140
        if (\is_array($flags)) {
141
            $flags = \implode(',', $flags);
142
        }
143
144
        $transaction = Craft::$app->getDb()->beginTransaction();
145
146
        try {
147
148
            $flagsId = (int)(new Query())
149
                ->select(['id'])
150
                ->from([Flags::tableName()])
151
                ->where([$sourceColumn => $sourceId])
152
                ->scalar();
153
154
            if ($flagsId) {
155
156
                Craft::$app->getDb()->createCommand()
157
                    ->update(
158
                        Flags::tableName(),
159
                        [
160
                            'flags' => $flags,
161
                        ],
162
                        [
163
                            $sourceColumn => $sourceId,
164
                        ],
165
                        null,
166
                        false
167
                    )
168
                    ->execute();
169
170
            } else {
171
172
                Craft::$app->getDb()->createCommand()
173
                    ->insert(
174
                        Flags::tableName(),
175
                        [
176
                            'flags' => $flags,
177
                            $sourceColumn => $sourceId,
178
                        ],
179
                        false)
180
                    ->execute();
181
            }
182
183
            $transaction->commit();
184
185
        } catch (\Throwable $e) {
186
            $transaction->rollBack();
187
188
            throw $e;
189
        }
190
    }
191
192
    /**
193
     * @param string $sourceColumn
194
     * @param string $sourceValue
195
     * @throws \Throwable
196
     * @throws \yii\db\Exception
197
     */
198
    public function deleteFlagsBySource(string $sourceColumn, string $sourceValue)
199
    {
200
201
        $transaction = Craft::$app->getDb()->beginTransaction();
202
203
        try {
204
205
            Craft::$app->getDb()->createCommand()
206
                ->delete(
207
                    Flags::tableName(),
208
                    [$sourceColumn => $sourceValue]
209
                )
210
                ->execute();
211
212
            $transaction->commit();
213
214
        } catch (\Throwable $e) {
215
            $transaction->rollBack();
216
217
            throw $e;
218
        }
219
    }
220
221
    /**
222
     * @param string|array $flags
223
     * @return bool
224
     */
225
    public function flagsHasCaches($flags): bool
226
    {
227
228
        $query = (new Query())
229
            ->select(['cacheId', 'flags'])
230
            ->from([Flagged::tableName()]);
231
232
        if (!\is_array($flags)) {
233
            $flags = \explode(',', \preg_replace('/\s+/', '', $flags));
234
        } else {
235
            $flags = \array_map(function ($flag) {
236
                return \preg_replace('/\s+/', '', $flag);
237
            }, $flags);
238
        }
239
240
        $dbDriver = Craft::$app->getDb()->getDriverName();
241
242
        foreach ($flags as $flag) {
243
            if ($dbDriver === 'pgsql') {
244
                $query->orWhere("'{$flag}' = ANY(string_to_array(flags, ','))");
245
            } else {
246
                $query->orWhere('FIND_IN_SET("' . $flag . '",flags)');
247
            }
248
        }
249
250
        return !!$query->scalar();
251
252
    }
253
254
    /**
255
     * @return bool
256
     */
257
    public function deleteAllFlaggedCaches(): bool
258
    {
259
        $cacheIds = (new Query())
260
            ->select(['cacheId'])
261
            ->from([Flagged::tableName()])
262
            ->column();
263
        if (!$cacheIds) {
264
            return true;
265
        }
266
        return Craft::$app->getTemplateCaches()->deleteCacheById($cacheIds);
267
    }
268
269
    /**
270
     * @param Element $element
271
     * @return bool
272
     */
273
    public function deleteFlaggedCachesByElement(Element $element): bool
274
    {
275
276
        // Collect all flags for this element
277
        $query = (new Query())
278
            ->select(['flags'])
279
            ->from(Flags::tableName());
280
281
        $elementType = \get_class($element);
282
283
        switch ($elementType) {
284
            case 'craft\elements\Asset':
285
                $query->orWhere([
286
                    'volumeId' => $element->volumeId,
287
                ]);
288
                break;
289
            case 'craft\elements\Category':
290
                $query->orWhere([
291
                    'categoryGroupId' => $element->groupId,
292
                ]);
293
                break;
294
            case 'craft\elements\Entry':
295
                $query->orWhere([
296
                    'sectionId' => $element->sectionId,
297
                ]);
298
                break;
299
            case 'craft\elements\GlobalSet':
300
                $query->orWhere([
301
                    'globalSetId' => $element->id,
302
                ]);
303
                break;
304
            case 'craft\elements\Tag':
305
                $query->orWhere([
306
                    'tagGroupId' => $element->groupId,
307
                ]);
308
                break;
309
            case 'craft\elements\User':
310
                foreach ($element->groups as $userGroup) {
311
                    $query->orWhere([
312
                        'userGroupId' => $userGroup->id,
313
                    ]);
314
                }
315
                break;
316
        }
317
318
        $query->orWhere([
319
            'elementType' => $elementType,
320
        ]);
321
322
        $flags = \array_unique($query->column());
323
324
        return $this->deleteFlaggedCachesByFlags($flags);
325
    }
326
327
    /**
328
     * @param $flags
329
     * @return bool
330
     */
331
    public function deleteFlaggedCachesByFlags($flags): bool
332
    {
333
334
        if (!$flags) {
335
            return false;
336
        }
337
338
        if (\is_array($flags)) {
339
            $flags = $this->implodeFlagsArray($flags);
340
        } else {
341
            $flags = \preg_replace('/\s+/', '', $flags);
342
        }
343
344
        $flags = \array_values(\array_unique(\array_filter(\explode(',', $flags))));
345
346
        $query = (new Query())
347
            ->select(['cacheId', 'flags'])
348
            ->from([Flagged::tableName()]);
349
350
        $dbDriver = Craft::$app->getDb()->getDriverName();
351
352
        foreach ($flags as $flag) {
353
            if ($dbDriver === 'pgsql') {
354
                $query->orWhere("'{$flag}' = ANY(string_to_array(flags, ','))");
355
            } else {
356
                $query->orWhere('FIND_IN_SET("' . $flag . '",flags)');
357
            }
358
        }
359
360
        $rows = $query->all();
361
362
        return $this->deleteCaches($rows);
363
364
    }
365
366
    /*
367
     * Protected methods
368
     */
369
    /**
370
     * @param array $flagsArray
371
     * @return string
372
     */
373
    protected function implodeFlagsArray(array $flagsArray): string
374
    {
375
376
        $flags = '';
377
378
        foreach ($flagsArray as $item) {
379
            if (\is_array($item)) {
380
                $flags .= "{$this->implodeFlagsArray($item, ',')},";
0 ignored issues
show
Unused Code introduced by
The call to mmikkel\cacheflag\servic...ce::implodeFlagsArray() has too many arguments starting with ','. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

380
                $flags .= "{$this->/** @scrutinizer ignore-call */ implodeFlagsArray($item, ',')},";

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
381
            } else {
382
                $flags .= \preg_replace('/\s+/', '', $item) . ',';
383
            }
384
        }
385
386
        $flags = \substr($flags, 0, 0 - strlen(','));
387
388
        return $flags;
389
    }
390
391
    /**
392
     * @param array $rows
393
     * @return bool
394
     */
395
    protected function deleteCaches(array $rows): bool
396
    {
397
398
        if (empty($rows)) {
399
            return true;
400
        }
401
402
        $cacheIds = [];
403
        $cacheFlags = [];
404
405
        foreach ($rows as $row) {
406
            $cacheIds[] = (int)$row['cacheId'];
407
            $cacheFlags = \array_merge($cacheFlags, \explode(',', $row['flags']));
408
        }
409
410
        $cacheFlags = \array_unique(\array_filter($cacheFlags));
411
412
        // Fire a 'beforeDeleteFlaggedCaches' event
413
        if ($this->hasEventHandlers(self::EVENT_BEFORE_DELETE_FLAGGED_CACHES)) {
414
            $this->trigger(self::EVENT_BEFORE_DELETE_FLAGGED_CACHES, new BeforeDeleteFlaggedTemplateCachesEvent([
415
                'cacheIds' => $cacheIds,
416
                'flags' => $cacheFlags,
417
            ]));
418
        }
419
420
        $success = Craft::$app->getTemplateCaches()->deleteCacheById($cacheIds);
421
422
        // Fire a 'afterDeleteFlaggedCaches' event
423
        if ($success && $this->hasEventHandlers(self::EVENT_AFTER_DELETE_FLAGGED_CACHES)) {
424
            $this->trigger(self::EVENT_AFTER_DELETE_FLAGGED_CACHES, new AfterDeleteFlaggedTemplateCachesEvent([
425
                'cacheIds' => $cacheIds,
426
                'flags' => $cacheFlags,
427
            ]));
428
        }
429
430
        return $success;
431
432
    }
433
}
434