ReasonsService   C
last analyzed

Complexity

Total Complexity 54

Size/Duplication

Total Lines 527
Duplicated Lines 0 %

Importance

Changes 13
Bugs 0 Features 0
Metric Value
eloc 244
c 13
b 0
f 0
dl 0
loc 527
rs 6.4799
wmc 54

18 Methods

Rating   Name   Duplication   Size   Complexity  
A normalizeConditionalsFromProjectConfig() 0 25 4
A onProjectConfigDelete() 0 25 3
A onProjectConfigRebuild() 0 27 5
A getData() 0 21 4
A onProjectConfigChange() 0 58 5
A deleteFieldLayoutConditionals() 0 18 2
A prepConditionalsForProjectConfig() 0 22 3
A clearCache() 0 3 1
A getCacheKey() 0 7 1
B getSources() 0 42 7
A getConditionals() 0 27 4
A saveFieldLayoutConditionals() 0 24 2
A getAllFields() 0 9 2
A getToggleFields() 0 21 4
A getFieldIdByUid() 0 10 2
A getToggleFieldTypes() 0 15 1
A getFieldUidById() 0 10 2
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\base\Field;
15
use craft\db\Query;
16
use craft\base\Component;
17
use craft\base\FieldInterface;
18
use craft\db\Table;
19
use craft\elements\User;
20
use craft\events\ConfigEvent;
21
use craft\events\RebuildConfigEvent;
22
use craft\fields\Assets;
23
use craft\fields\Categories;
24
use craft\fields\Checkboxes;
25
use craft\fields\Dropdown;
26
use craft\fields\Entries;
27
use craft\fields\Number;
28
use craft\fields\Lightswitch;
29
use craft\fields\MultiSelect;
30
use craft\fields\PlainText;
31
use craft\fields\RadioButtons;
32
use craft\fields\Tags;
33
use craft\fields\Users;
34
use craft\helpers\Db;
35
use craft\helpers\Json;
36
use craft\helpers\StringHelper;
37
use craft\models\FieldLayout;
38
use craft\records\EntryType;
39
40
use mmikkel\reasons\Reasons;
41
42
/**
43
 * @author    Mats Mikkel Rummelhoff
44
 * @package   Reasons
45
 * @since     2.0.0
46
 */
47
class ReasonsService extends Component
48
{
49
50
    /** @var int */
51
    const CACHE_TTL = 1800;
52
53
    /** @var Field[] */
54
    protected $allFields;
55
56
    /** @var Field[] */
57
    protected $toggleFields;
58
59
    /** @var array */
60
    protected $sources;
61
62
    /** @var int[] */
63
    protected $fieldIdsByUid;
64
65
    /** @var string[] */
66
    protected $fieldUidsById;
67
68
    // Public Methods
69
    // =========================================================================
70
71
    /**
72
     * Saves a field layout's conditionals, via the Project Config
73
     *
74
     * @param FieldLayout $layout
75
     * @param string|array $conditionals
76
     * @return bool
77
     * @throws \yii\base\ErrorException
78
     * @throws \yii\base\Exception
79
     * @throws \yii\base\NotSupportedException
80
     * @throws \yii\web\ServerErrorHttpException
81
     */
82
    public function saveFieldLayoutConditionals(FieldLayout $layout, $conditionals): bool
83
    {
84
85
        $uid = (new Query())
86
            ->select(['uid'])
87
            ->from('{{%reasons}}')
88
            ->where(['fieldLayoutId' => $layout->id])
89
            ->scalar();
90
91
        $isNew = !$uid;
92
        if ($isNew) {
93
            $uid = StringHelper::UUID();
94
        }
95
96
        $conditionals = $this->prepConditionalsForProjectConfig($conditionals);
97
98
        // Save it to the project config
99
        $path = "reasons_conditionals.{$uid}";
100
        Craft::$app->getProjectConfig()->set($path, [
101
            'fieldLayoutUid' => $layout->uid,
102
            'conditionals' => $conditionals,
103
        ]);
104
105
        return true;
106
    }
107
108
    /**
109
     * Deletes a field layout's conditionals, via the Project Config
110
     *
111
     * @param FieldLayout $layout
112
     * @return bool
113
     */
114
    public function deleteFieldLayoutConditionals(FieldLayout $layout): bool
115
    {
116
117
        $uid = (new Query())
118
            ->select(['uid'])
119
            ->from('{{%reasons}}')
120
            ->where(['fieldLayoutId' => $layout->id])
121
            ->scalar();
122
123
        if (!$uid) {
124
            return false;
125
        }
126
127
        // Remove it from the project config
128
        $path = "reasons_conditionals.{$uid}";
129
        Craft::$app->getProjectConfig()->remove($path);
130
131
        return true;
132
    }
133
134
    /**
135
     * @param ConfigEvent $event
136
     * @return void
137
     * @throws \Throwable
138
     * @throws \yii\db\Exception
139
     */
140
    public function onProjectConfigChange(ConfigEvent $event)
141
    {
142
143
        $this->clearCache();
144
145
        $uid = $event->tokenMatches[0];
146
        $data = $event->newValue;
147
148
        $id = (new Query())
149
            ->select(['id'])
150
            ->from('{{%reasons}}')
151
            ->where(['uid' => $uid])
152
            ->scalar();
153
154
        $isNew = empty($id);
155
156
        if ($isNew) {
157
158
            // Save new conditionals
159
            $fieldLayoutId = Db::idByUid(Table::FIELDLAYOUTS, $data['fieldLayoutUid']);
160
161
            if ($fieldLayoutId === null) {
162
                // The field layout might not've synced yet. Defer to Project Config
163
                Craft::$app->getProjectConfig()->defer($event, [$this, __FUNCTION__]);
164
                return;
165
            }
166
167
            $transaction = Craft::$app->getDb()->beginTransaction();
168
169
            try {
170
                Craft::$app->db->createCommand()
171
                    ->insert('{{%reasons}}', [
172
                        'fieldLayoutId' => $fieldLayoutId,
173
                        'conditionals' => $data['conditionals'],
174
                        'uid' => $uid,
175
                    ])
176
                    ->execute();
177
                $transaction->commit();
178
            } catch (\Throwable $e) {
179
                $transaction->rollBack();
180
                throw $e;
181
            }
182
183
        } else {
184
185
            // Update existing conditionals
186
            $transaction = Craft::$app->getDb()->beginTransaction();
187
188
            try {
189
                Craft::$app->db->createCommand()
190
                    ->update('{{%reasons}}', [
191
                        'conditionals' => $data['conditionals'],
192
                    ], ['id' => $id])
193
                    ->execute();
194
                $transaction->commit();
195
            } catch (\Throwable $e) {
196
                $transaction->rollBack();
197
                throw $e;
198
            }
199
        }
200
201
    }
202
203
    /**
204
     * @param ConfigEvent $event
205
     * @return void
206
     * @throws \Throwable
207
     * @throws \yii\db\Exception
208
     */
209
    public function onProjectConfigDelete(ConfigEvent $event)
210
    {
211
212
        $this->clearCache();
213
214
        $uid = $event->tokenMatches[0];
215
216
        $id = (new Query())
217
            ->select(['id'])
218
            ->from('{{%reasons}}')
219
            ->where(['uid' => $uid])
220
            ->scalar();
221
222
        if ($id) {
223
224
            $transaction = Craft::$app->getDb()->beginTransaction();
225
226
            try {
227
                Craft::$app->db->createCommand()
228
                    ->delete('{{%reasons}}', ['id' => $id])
229
                    ->execute();
230
                $transaction->commit();
231
            } catch (\Throwable $e) {
232
                $transaction->rollBack();
233
                throw $e;
234
            }
235
        }
236
237
    }
238
239
    /**
240
     * @param RebuildConfigEvent $event
241
     * @return void
242
     */
243
    public function onProjectConfigRebuild(RebuildConfigEvent $event)
244
    {
245
246
        Craft::$app->getProjectConfig()->remove('reasons_conditionals');
247
248
        $event->config['reasons_conditionals'] = [];
249
250
        $rows = (new Query())
251
            ->select(['reasons.uid', 'reasons.conditionals', 'fieldlayouts.uid AS fieldLayoutUid'])
252
            ->from('{{%reasons}} AS reasons')
253
            ->innerJoin(['fieldlayouts' => Table::FIELDLAYOUTS], '[[fieldlayouts.id]] = [[reasons.fieldLayoutId]]')
254
            ->all();
255
256
        foreach ($rows as $row) {
257
            $uid = $row['uid'] ?? null;
258
            $conditionals = $row['conditionals'] ?? null;
259
            $fieldLayoutUid = $row['fieldLayoutUid'] ?? null;
260
            if (!$uid || !$conditionals || !$fieldLayoutUid) {
261
                continue;
262
            }
263
            $event->config['reasons_conditionals'][$uid] = [
264
                'conditionals' => $conditionals,
265
                'fieldLayoutUid' => $fieldLayoutUid,
266
            ];
267
        }
268
269
        $this->clearCache();
270
    }
271
272
    /**
273
     * Clears Reasons' data caches
274
     *
275
     * @return void
276
     */
277
    public function clearCache()
278
    {
279
        Craft::$app->getCache()->delete($this->getCacheKey());
280
    }
281
282
    /**
283
     * @return array|mixed
284
     */
285
    public function getData()
286
    {
287
        $doCacheData = !Craft::$app->getConfig()->getGeneral()->devMode;
288
        $cacheKey = $this->getCacheKey();
289
290
        if ($doCacheData && $data = Craft::$app->getCache()->get($cacheKey)) {
291
            return $data;
292
        }
293
294
        $data = [
295
            'conditionals' => $this->getConditionals(),
296
            'toggleFieldTypes' => $this->getToggleFieldTypes(),
297
            'toggleFields' => $this->getToggleFields(),
298
            'fieldIds' => $this->getFieldIdsByHandle(),
299
        ];
300
301
        if ($doCacheData) {
302
            Craft::$app->getCache()->set($cacheKey, $data, self::CACHE_TTL);
303
        }
304
305
        return $data;
306
    }
307
308
    /**
309
     * @param string|array $conditionals
310
     * @return string|null
311
     */
312
    protected function prepConditionalsForProjectConfig($conditionals): ?string
313
    {
314
        if (!$conditionals) {
315
            return null;
316
        }
317
        $return = [];
318
        $conditionals = Json::decodeIfJson($conditionals);
319
        foreach ($conditionals as $targetFieldId => $statements) {
320
            $targetFieldId = (int)$targetFieldId;
321
            $targetFieldUid = $this->getFieldUidById($targetFieldId);
322
            $return[$targetFieldUid] = \array_map(function (array $rules) {
323
                return \array_map(function (array $rule) {
324
                    $fieldId = (int)$rule['fieldId'];
325
                    return [
326
                        'field' => $this->getFieldUidById($fieldId),
327
                        'compare' => $rule['compare'],
328
                        'value' => $rule['value'],
329
                    ];
330
                }, $rules);
331
            }, $statements);
332
        }
333
        return Json::encode($return);
334
    }
335
336
    /**
337
     * @param string|array $conditionals
338
     * @return array|null
339
     */
340
    protected function normalizeConditionalsFromProjectConfig($conditionals): ?array
341
    {
342
        if (!$conditionals) {
343
            return null;
344
        }
345
        $return = [];
346
        try {
347
            $conditionals = Json::decodeIfJson($conditionals);
348
            foreach ($conditionals as $targetFieldUid => $statements) {
349
                $targetFieldId = $this->getFieldIdByUid($targetFieldUid);
350
                $return["$targetFieldId"] = \array_map(function (array $rules) {
351
                    return \array_map(function (array $rule) {
352
                        $fieldUid = $rule['field'];
353
                        return [
354
                            'fieldId' => $this->getFieldIdByUid($fieldUid),
355
                            'compare' => $rule['compare'],
356
                            'value' => $rule['value'],
357
                        ];
358
                    }, $rules);
359
                }, $statements);
360
            }
361
        } catch (\Throwable $e) {
362
            Craft::error($e->getMessage(), __METHOD__);
363
        }
364
        return $return;
365
    }
366
367
    /**
368
     * Returns all conditionals, mapped by source key
369
     *
370
     * @return array
371
     */
372
    protected function getConditionals(): array
373
    {
374
375
        // Get all conditionals from database
376
        $rows = (new Query())
377
            ->select(['reasons.id', 'reasons.fieldLayoutId', 'reasons.conditionals'])
378
            ->from('{{%reasons}} AS reasons')
379
            ->innerJoin(['fieldlayouts' => Table::FIELDLAYOUTS], '[[fieldlayouts.id]] = [[reasons.fieldLayoutId]]')
380
            ->all();
381
382
        // Map conditionals to field layouts, and convert field uids to ids
383
        $conditionals = [];
384
        foreach ($rows as $row) {
385
            $conditionals["fieldLayout:{$row['fieldLayoutId']}"] = $this->normalizeConditionalsFromProjectConfig($row['conditionals']);
386
        }
387
388
        // Map conditionals to sources
389
        $conditionalsBySources = [];
390
        $sources = $this->getSources();
391
        foreach ($sources as $sourceId => $fieldLayoutId) {
392
            if (!isset($conditionals["fieldLayout:{$fieldLayoutId}"])) {
393
                continue;
394
            }
395
            $conditionalsBySources[$sourceId] = $conditionals["fieldLayout:{$fieldLayoutId}"];
396
        }
397
398
        return $conditionalsBySources;
399
    }
400
401
    /**
402
     * @return array
403
     */
404
    protected function getSources(): array
405
    {
406
407
        if (!isset($this->sources)) {
408
409
            $sources = [];
410
411
            $entryTypeRecords = EntryType::find()->all();
412
            /** @var EntryType $entryTypeRecord */
413
            foreach ($entryTypeRecords as $entryTypeRecord) {
414
                $sources["entryType:{$entryTypeRecord->id}"] = (int)$entryTypeRecord->fieldLayoutId;
415
                $sources["section:{$entryTypeRecord->sectionId}"] = (int)$entryTypeRecord->fieldLayoutId;
416
            }
417
418
            $categoryGroups = Craft::$app->getCategories()->getAllGroups();
419
            foreach ($categoryGroups as $categoryGroup) {
420
                $sources["categoryGroup:{$categoryGroup->id}"] = (int)$categoryGroup->fieldLayoutId;
421
            }
422
423
            $tagGroups = Craft::$app->getTags()->getAllTagGroups();
424
            foreach ($tagGroups as $tagGroup) {
425
                $sources["tagGroup:{$tagGroup->id}"] = (int)$tagGroup->fieldLayoutId;
426
            }
427
428
            $volumes = Craft::$app->getVolumes()->getAllVolumes();
429
            foreach ($volumes as $volume) {
430
                $sources["assetSource:{$volume->id}"] = (int)$volume->fieldLayoutId;
431
            }
432
433
            $globalSets = Craft::$app->getGlobals()->getAllSets();
434
            foreach ($globalSets as $globalSet) {
435
                $sources["globalSet:{$globalSet->id}"] = (int)$globalSet->fieldLayoutId;
436
            }
437
438
            $usersFieldLayout = Craft::$app->getFields()->getLayoutByType(User::class);
439
            $sources['users'] = $usersFieldLayout->id;
440
441
            $this->sources = $sources;
442
443
        }
444
445
        return $this->sources;
446
    }
447
448
    /**
449
     * Returns all toggleable fields
450
     *
451
     * @return array
452
     */
453
    protected function getToggleFields(): array
454
    {
455
        if (!isset($this->toggleFields)) {
456
            $this->toggleFields = [];
457
            $fields = $this->getAllFields();
458
            $toggleFieldTypes = $this->getToggleFieldTypes();
459
            foreach ($fields as $field) {
460
                $fieldType = \get_class($field);
461
                if (!\in_array($fieldType, $toggleFieldTypes)) {
462
                    continue;
463
                }
464
                $this->toggleFields[] = [
465
                    'id' => (int)$field->id,
466
                    'handle' => $field->handle,
467
                    'name' => $field->name,
468
                    'type' => $fieldType,
469
                    'settings' => $field->getSettings(),
470
                ];
471
            }
472
        }
473
        return $this->toggleFields;
474
    }
475
476
    /**
477
     * Returns all toggleable fieldtype classnames
478
     *
479
     * @return string[]
480
     */
481
    protected function getToggleFieldTypes(): array
482
    {
483
        return [
484
            Lightswitch::class,
485
            Dropdown::class,
486
            Checkboxes::class,
487
            MultiSelect::class,
488
            RadioButtons::class,
489
            Number::class,
490
            PlainText::class,
491
            Entries::class,
492
            Categories::class,
493
            Tags::class,
494
            Assets::class,
495
            Users::class,
496
        ];
497
    }
498
499
    /**
500
     * Returns all global field IDs, indexed by handle
501
     *
502
     * @return array
503
     */
504
    protected function getFieldIdsByHandle(): array
505
    {
506
        $handles = [];
507
        $fields = $this->getAllFields();
508
        foreach ($fields as $field) {
509
            $handles[$field->handle] = (int)$field->id;
510
        }
511
        return $handles;
512
    }
513
514
    /**
515
     * @return Field[]
516
     */
517
    protected function getAllFields(): array
518
    {
519
        if (!isset($this->allFields)) {
520
            $globalFields = Craft::$app->getFields()->getAllFields('global');
521
            $this->allFields = \array_filter($globalFields, function (FieldInterface $field) {
522
                return $field instanceof Field;
523
            });
524
        }
525
        return $this->allFields;
526
    }
527
528
    /**
529
     * Return the UID for a field, based on its database ID
530
     *
531
     * @param int $fieldId
532
     * @return string
533
     */
534
    protected function getFieldUidById(int $fieldId): string
535
    {
536
        if (!isset($this->fieldUidsById)) {
537
            $allFields = $this->getAllFields();
538
            $this->fieldUidsById = \array_reduce($allFields, function (array $carry, Field $field) {
539
                $carry["{$field->id}"] = $field->uid;
540
                return $carry;
541
            }, []);
542
        }
543
        return $this->fieldUidsById["$fieldId"];
544
    }
545
546
    /**
547
     * Return the database ID for a field, based on its UID
548
     *
549
     * @param string $fieldUid
550
     * @return int
551
     */
552
    protected function getFieldIdByUid(string $fieldUid): int
553
    {
554
        if (!isset($this->fieldIdsByUid)) {
555
            $allFields = $this->getAllFields();
556
            $this->fieldIdsByUid = \array_reduce($allFields, function (array $carry, Field $field) {
557
                $carry[$field->uid] = (int)$field->id;
558
                return $carry;
559
            }, []);
560
        }
561
        return $this->fieldIdsByUid[$fieldUid];
562
    }
563
564
    /**
565
     * @return string
566
     */
567
    protected function getCacheKey(): string
568
    {
569
        $reasons = Reasons::getInstance();
570
        return \implode('-', [
571
            $reasons->getHandle(),
572
            $reasons->getVersion(),
573
            $reasons->schemaVersion
574
        ]);
575
    }
576
}
577