Passed
Push — master ( 207640...c55bfc )
by M. Mikkel
04:28
created

ReasonsService   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 471
Duplicated Lines 0 %

Importance

Changes 8
Bugs 0 Features 0
Metric Value
eloc 223
c 8
b 0
f 0
dl 0
loc 471
rs 8.48
wmc 49

16 Methods

Rating   Name   Duplication   Size   Complexity  
A deleteFieldLayoutConditionals() 0 18 2
A saveFieldLayoutConditionals() 0 24 2
A normalizeConditionalsFromProjectConfig() 0 24 4
A getAllFields() 0 6 2
A getToggleFields() 0 20 3
A onProjectConfigDelete() 0 25 3
A onProjectConfigRebuild() 0 27 5
A getData() 0 21 4
A onProjectConfigChange() 0 58 5
A prepConditionalsForProjectConfig() 0 20 3
A clearCache() 0 3 1
A getCacheKey() 0 7 1
B getSources() 0 42 7
A getConditionals() 0 27 4
A getToggleFieldTypes() 0 15 1
A getFieldIdsByHandle() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like ReasonsService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ReasonsService, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Reasons plugin for Craft CMS 3.x
4
 *
5
 * Adds conditionals to field layouts.
6
 *
7
 * @link      https://vaersaagod.no
8
 * @copyright Copyright (c) 2020 Mats Mikkel Rummelhoff
9
 */
10
11
namespace mmikkel\reasons\services;
12
13
use Craft;
14
use craft\db\Query;
15
use craft\base\Component;
16
use craft\base\FieldInterface;
17
use craft\elements\User;
18
use craft\events\ConfigEvent;
19
use craft\events\RebuildConfigEvent;
20
use craft\fields\Assets;
21
use craft\fields\Categories;
22
use craft\fields\Checkboxes;
23
use craft\fields\Dropdown;
24
use craft\fields\Entries;
25
use craft\fields\Number;
26
use craft\fields\Lightswitch;
27
use craft\fields\MultiSelect;
28
use craft\fields\PlainText;
29
use craft\fields\RadioButtons;
30
use craft\fields\Tags;
31
use craft\fields\Users;
32
use craft\helpers\Db;
33
use craft\helpers\Json;
34
use craft\helpers\StringHelper;
35
use craft\models\FieldLayout;
36
use craft\records\EntryType;
37
38
use mmikkel\reasons\Reasons;
39
40
/**
41
 * @author    Mats Mikkel Rummelhoff
42
 * @package   Reasons
43
 * @since     2.0.0
44
 */
45
class ReasonsService extends Component
46
{
47
48
    /** @var int */
49
    const CACHE_TTL = 1800;
50
51
    /** @var array */
52
    protected $allFields;
53
54
    /** @var array */
55
    protected $sources;
56
57
    // Public Methods
58
    // =========================================================================
59
60
    /**
61
     * Saves a field layout's conditionals, via the Project Config
62
     *
63
     * @param FieldLayout $layout
64
     * @param string|array $conditionals
65
     * @return bool
66
     * @throws \yii\base\ErrorException
67
     * @throws \yii\base\Exception
68
     * @throws \yii\base\NotSupportedException
69
     * @throws \yii\web\ServerErrorHttpException
70
     */
71
    public function saveFieldLayoutConditionals(FieldLayout $layout, $conditionals): bool
72
    {
73
74
        $uid = (new Query())
75
            ->select(['uid'])
76
            ->from('{{%reasons}}')
77
            ->where(['fieldLayoutId' => $layout->id])
78
            ->scalar();
79
80
        $isNew = !$uid;
81
        if ($isNew) {
82
            $uid = StringHelper::UUID();
83
        }
84
85
        $conditionals = $this->prepConditionalsForProjectConfig($conditionals);
86
87
        // Save it to the project config
88
        $path = "reasons_conditionals.{$uid}";
89
        Craft::$app->projectConfig->set($path, [
0 ignored issues
show
Bug introduced by
The method set() does not exist on null. ( Ignorable by Annotation )

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

89
        Craft::$app->projectConfig->/** @scrutinizer ignore-call */ 
90
                                    set($path, [

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
90
            'fieldLayoutUid' => $layout->uid,
91
            'conditionals' => $conditionals,
92
        ]);
93
94
        return true;
95
    }
96
97
    /**
98
     * Deletes a field layout's conditionals, via the Project Config
99
     *
100
     * @param FieldLayout $layout
101
     * @return bool
102
     */
103
    public function deleteFieldLayoutConditionals(FieldLayout $layout): bool
104
    {
105
106
        $uid = (new Query())
107
            ->select(['uid'])
108
            ->from('{{%reasons}}')
109
            ->where(['fieldLayoutId' => $layout->id])
110
            ->scalar();
111
112
        if (!$uid) {
113
            return false;
114
        }
115
116
        // Remove it from the project config
117
        $path = "reasons_conditionals.{$uid}";
118
        Craft::$app->projectConfig->remove($path);
119
120
        return true;
121
    }
122
123
    /**
124
     * @param ConfigEvent $event
125
     * @throws \yii\db\Exception
126
     */
127
    public function onProjectConfigChange(ConfigEvent $event)
128
    {
129
130
        $this->clearCache();
131
132
        $uid = $event->tokenMatches[0];
133
        $data = $event->newValue;
134
135
        $id = (new Query())
136
            ->select(['id'])
137
            ->from('{{%reasons}}')
138
            ->where(['uid' => $uid])
139
            ->scalar();
140
141
        $isNew = empty($id);
142
143
        if ($isNew) {
144
145
            // Save new conditionals
146
            $fieldLayoutId = Db::idByUid('{{%fieldlayouts}}', $data['fieldLayoutUid']);
147
148
            if ($fieldLayoutId === null) {
149
                // The field layout might not've synced yet. Defer to Project Config
150
                Craft::$app->getProjectConfig()->defer($event, [$this, __FUNCTION__]);
151
                return;
152
            }
153
154
            $transaction = Craft::$app->getDb()->beginTransaction();
155
156
            try {
157
                Craft::$app->db->createCommand()
158
                    ->insert('{{%reasons}}', [
159
                        'fieldLayoutId' => $fieldLayoutId,
160
                        'conditionals' => $data['conditionals'],
161
                        'uid' => $uid,
162
                    ])
163
                    ->execute();
164
                $transaction->commit();
165
            } catch (\Throwable $e) {
166
                $transaction->rollBack();
167
                throw $e;
168
            }
169
170
        } else {
171
172
            // Update existing conditionals
173
            $transaction = Craft::$app->getDb()->beginTransaction();
174
175
            try {
176
                Craft::$app->db->createCommand()
177
                    ->update('{{%reasons}}', [
178
                        'conditionals' => $data['conditionals'],
179
                    ], ['id' => $id])
180
                    ->execute();
181
                $transaction->commit();
182
            } catch (\Throwable $e) {
183
                $transaction->rollBack();
184
                throw $e;
185
            }
186
        }
187
188
    }
189
190
    /**
191
     * @param ConfigEvent $event
192
     * @throws \yii\db\Exception
193
     */
194
    public function onProjectConfigDelete(ConfigEvent $event)
195
    {
196
197
        $this->clearCache();
198
199
        $uid = $event->tokenMatches[0];
200
201
        $id = (new Query())
202
            ->select(['id'])
203
            ->from('{{%reasons}}')
204
            ->where(['uid' => $uid])
205
            ->scalar();
206
207
        if ($id) {
208
209
            $transaction = Craft::$app->getDb()->beginTransaction();
210
211
            try {
212
                Craft::$app->db->createCommand()
213
                    ->delete('{{%reasons}}', ['id' => $id])
214
                    ->execute();
215
                $transaction->commit();
216
            } catch (\Throwable $e) {
217
                $transaction->rollBack();
218
                throw $e;
219
            }
220
        }
221
222
    }
223
224
    /**
225
     * @param RebuildConfigEvent $event
226
     * @return void
227
     */
228
    public function onProjectConfigRebuild(RebuildConfigEvent $event)
229
    {
230
231
        Craft::$app->getProjectConfig()->remove('reasons_conditionals');
232
233
        $event->config['reasons_conditionals'] = [];
234
235
        $rows = (new Query())
236
            ->select(['reasons.uid', 'reasons.conditionals', 'fieldlayouts.uid AS fieldLayoutUid'])
237
            ->from('{{%reasons}} AS reasons')
238
            ->innerJoin('{{%fieldlayouts}} AS fieldlayouts', 'fieldlayouts.id = reasons.fieldLayoutId')
239
            ->all();
240
241
        foreach ($rows as $row) {
242
            $uid = $row['uid'] ?? null;
243
            $conditionals = $row['conditionals'] ?? null;
244
            $fieldLayoutUid = $row['fieldLayoutUid'] ?? null;
245
            if (!$uid || !$conditionals || !$fieldLayoutUid) {
246
                continue;
247
            }
248
            $event->config['reasons_conditionals'][$uid] = [
249
                'conditionals' => $conditionals,
250
                'fieldLayoutUid' => $fieldLayoutUid,
251
            ];
252
        }
253
254
        $this->clearCache();
255
    }
256
257
    /**
258
     * Clears Reasons' data caches
259
     *
260
     * @return void
261
     */
262
    public function clearCache()
263
    {
264
        Craft::$app->getCache()->delete($this->getCacheKey());
265
    }
266
267
    /**
268
     * @return array|mixed
269
     */
270
    public function getData()
271
    {
272
        $doCacheData = !Craft::$app->getConfig()->getGeneral()->devMode;
273
        $cacheKey = $this->getCacheKey();
274
275
        if ($doCacheData && $data = Craft::$app->getCache()->get($cacheKey)) {
276
            return $data;
277
        }
278
279
        $data = [
280
            'conditionals' => $this->getConditionals(),
281
            'toggleFieldTypes' => $this->getToggleFieldTypes(),
282
            'toggleFields' => $this->getToggleFields(),
283
            'fieldIds' => $this->getFieldIdsByHandle(),
284
        ];
285
286
        if ($doCacheData) {
287
            Craft::$app->getCache()->set($cacheKey, $data, self::CACHE_TTL);
288
        }
289
290
        return $data;
291
    }
292
293
    /**
294
     * @param string|array $conditionals
295
     * @return string|null
296
     */
297
    protected function prepConditionalsForProjectConfig($conditionals)
298
    {
299
        if (!$conditionals) {
300
            return null;
301
        }
302
        $return = [];
303
        $conditionals = Json::decodeIfJson($conditionals);
304
        foreach ($conditionals as $targetFieldId => $statements) {
305
            $targetFieldUid = Db::uidById('{{%fields}}', $targetFieldId);
306
            $return[$targetFieldUid] = \array_map(function (array $rules) {
307
                return \array_map(function (array $rule) {
308
                    return [
309
                        'field' => Db::uidById('{{%fields}}', $rule['fieldId']),
310
                        'compare' => $rule['compare'],
311
                        'value' => $rule['value'],
312
                    ];
313
                }, $rules);
314
            }, $statements);
315
        }
316
        return Json::encode($return);
317
    }
318
319
    /**
320
     * @param string|array $conditionals
321
     * @return array|null
322
     */
323
    protected function normalizeConditionalsFromProjectConfig($conditionals)
324
    {
325
        if (!$conditionals) {
326
            return null;
327
        }
328
        $return = [];
329
        try {
330
            $conditionals = Json::decodeIfJson($conditionals);
331
            foreach ($conditionals as $targetFieldUid => $statements) {
332
                $targetFieldId = Db::idByUid('{{%fields}}', $targetFieldUid);
333
                $return[$targetFieldId] = \array_map(function (array $rules) {
334
                    return \array_map(function (array $rule) {
335
                        return [
336
                            'fieldId' => Db::idByUid('{{%fields}}', $rule['field']),
337
                            'compare' => $rule['compare'],
338
                            'value' => $rule['value'],
339
                        ];
340
                    }, $rules);
341
                }, $statements);
342
            }
343
        } catch (\Throwable $e) {
344
            Craft::error($e->getMessage(), __METHOD__);
345
        }
346
        return $return;
347
    }
348
349
    /**
350
     * Returns all conditionals, mapped by source key
351
     *
352
     * @return array
353
     */
354
    protected function getConditionals(): array
355
    {
356
357
        // Get all conditionals from database
358
        $rows = (new Query())
359
            ->select(['reasons.id', 'reasons.fieldLayoutId', 'reasons.conditionals'])
360
            ->from('{{%reasons}} AS reasons')
361
            ->innerJoin('{{%fieldlayouts}} AS fieldlayouts', 'fieldlayouts.id = fieldLayoutId')
362
            ->all();
363
364
        // Map conditionals to field layouts, and convert field uids to ids
365
        $conditionals = [];
366
        foreach ($rows as $row) {
367
            $conditionals["fieldLayout:{$row['fieldLayoutId']}"] = $this->normalizeConditionalsFromProjectConfig($row['conditionals']);
368
        }
369
370
        // Map conditionals to sources
371
        $conditionalsBySources = [];
372
        $sources = $this->getSources();
373
        foreach ($sources as $sourceId => $fieldLayoutId) {
374
            if (!isset($conditionals["fieldLayout:{$fieldLayoutId}"])) {
375
                continue;
376
            }
377
            $conditionalsBySources[$sourceId] = $conditionals["fieldLayout:{$fieldLayoutId}"];
378
        }
379
380
        return $conditionalsBySources;
381
    }
382
383
    /**
384
     * @return array
385
     */
386
    protected function getSources(): array
387
    {
388
389
        if (!isset($this->sources)) {
390
391
            $sources = [];
392
393
            $entryTypeRecords = EntryType::find()->all();
394
            /** @var EntryType $entryTypeRecord */
395
            foreach ($entryTypeRecords as $entryTypeRecord) {
396
                $sources["entryType:{$entryTypeRecord->id}"] = (int)$entryTypeRecord->fieldLayoutId;
0 ignored issues
show
Bug introduced by
Accessing fieldLayoutId on the interface yii\db\ActiveRecordInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
Bug introduced by
Accessing id on the interface yii\db\ActiveRecordInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
397
                $sources["section:{$entryTypeRecord->sectionId}"] = (int)$entryTypeRecord->fieldLayoutId;
0 ignored issues
show
Bug introduced by
Accessing sectionId on the interface yii\db\ActiveRecordInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
398
            }
399
400
            $categoryGroups = Craft::$app->getCategories()->getAllGroups();
401
            foreach ($categoryGroups as $categoryGroup) {
402
                $sources["categoryGroup:{$categoryGroup->id}"] = (int)$categoryGroup->fieldLayoutId;
403
            }
404
405
            $tagGroups = Craft::$app->getTags()->getAllTagGroups();
406
            foreach ($tagGroups as $tagGroup) {
407
                $sources["tagGroup:{$tagGroup->id}"] = (int)$tagGroup->fieldLayoutId;
408
            }
409
410
            $volumes = Craft::$app->getVolumes()->getAllVolumes();
411
            foreach ($volumes as $volume) {
412
                $sources["assetSource:{$volume->id}"] = (int)$volume->fieldLayoutId;
413
            }
414
415
            $globalSets = Craft::$app->getGlobals()->getAllSets();
416
            foreach ($globalSets as $globalSet) {
417
                $sources["globalSet:{$globalSet->id}"] = (int)$globalSet->fieldLayoutId;
418
            }
419
420
            $usersFieldLayout = Craft::$app->getFields()->getLayoutByType(User::class);
421
            $sources['users'] = $usersFieldLayout->id;
422
423
            $this->sources = $sources;
424
425
        }
426
427
        return $this->sources;
428
    }
429
430
    /**
431
     * Returns all toggleable fields
432
     *
433
     * @return array
434
     */
435
    protected function getToggleFields(): array
436
    {
437
        $toggleFieldTypes = $this->getToggleFieldTypes();
438
        $toggleFields = [];
439
        $fields = $this->getAllFields();
440
        /** @var FieldInterface $field */
441
        foreach ($fields as $field) {
442
            $fieldType = \get_class($field);
443
            if (!\in_array($fieldType, $toggleFieldTypes)) {
444
                continue;
445
            }
446
            $toggleFields[] = [
447
                'id' => (int)$field->id,
448
                'handle' => $field->handle,
449
                'name' => $field->name,
450
                'type' => $fieldType,
451
                'settings' => $field->getSettings(),
452
            ];
453
        }
454
        return $toggleFields;
455
    }
456
457
    /**
458
     * Returns all toggleable fieldtype classnames
459
     *
460
     * @return string[]
461
     */
462
    protected function getToggleFieldTypes(): array
463
    {
464
        return [
465
            Lightswitch::class,
466
            Dropdown::class,
467
            Checkboxes::class,
468
            MultiSelect::class,
469
            RadioButtons::class,
470
            Number::class,
471
            PlainText::class,
472
            Entries::class,
473
            Categories::class,
474
            Tags::class,
475
            Assets::class,
476
            Users::class,
477
        ];
478
    }
479
480
    /**
481
     * Returns all global field IDs, indexed by handle
482
     *
483
     * @return array
484
     */
485
    protected function getFieldIdsByHandle(): array
486
    {
487
        $handles = [];
488
        $fields = $this->getAllFields();
489
        foreach ($fields as $field) {
490
            $handles[$field->handle] = (int)$field->id;
491
        }
492
        return $handles;
493
    }
494
495
    /**
496
     * @return FieldInterface[]
497
     */
498
    protected function getAllFields(): array
499
    {
500
        if (!isset($this->allFields)) {
501
            $this->allFields = Craft::$app->getFields()->getAllFields('global');
502
        }
503
        return $this->allFields;
504
    }
505
506
    /**
507
     * @return string
508
     */
509
    protected function getCacheKey(): string
510
    {
511
        $reasons = Reasons::getInstance();
512
        return \implode('-', [
513
            $reasons->getHandle(),
514
            $reasons->getVersion(),
515
            $reasons->schemaVersion
516
        ]);
517
    }
518
}
519