Passed
Push — master ( c519bc...9936ed )
by M. Mikkel
05:18
created

ReasonsService::onProjectConfigChange()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 31
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 22
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 31
rs 9.568
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 mmikkel\reasons\Reasons;
14
15
use Craft;
16
use craft\db\Query;
17
use craft\base\Component;
18
use craft\base\FieldInterface;
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
/**
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, [
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
        $uid = $event->tokenMatches[0];
131
132
        $id = (new Query())
133
            ->select(['id'])
134
            ->from('{{%reasons}}')
135
            ->where(['uid' => $uid])
136
            ->scalar();
137
138
        $isNew = empty($id);
139
140
        if ($isNew) {
141
            $fieldLayoutId = (int)Db::idByUid('{{%fieldlayouts}}', $event->newValue['fieldLayoutUid']);
142
            Craft::$app->db->createCommand()
143
                ->insert('{{%reasons}}', [
144
                    'fieldLayoutId' => $fieldLayoutId,
145
                    'conditionals' => $event->newValue['conditionals'],
146
                    'uid' => $uid,
147
                ])
148
                ->execute();
149
        } else {
150
            Craft::$app->db->createCommand()
151
                ->update('{{%reasons}}', [
152
                    'conditionals' => $event->newValue['conditionals'],
153
                ], ['id' => $id])
154
                ->execute();
155
        }
156
157
        $this->clearCache();
158
159
    }
160
161
    /**
162
     * @param ConfigEvent $event
163
     * @throws \yii\db\Exception
164
     */
165
    public function onProjectConfigDelete(ConfigEvent $event)
166
    {
167
168
        $uid = $event->tokenMatches[0];
169
170
        $id = (new Query())
171
            ->select(['id'])
172
            ->from('{{%reasons}}')
173
            ->where(['uid' => $uid])
174
            ->scalar();
175
176
        if (!$id) {
177
            return;
178
        }
179
180
        Craft::$app->db->createCommand()
181
            ->delete('{{%reasons}}', ['id' => $id])
182
            ->execute();
183
184
        $this->clearCache();
185
186
    }
187
188
    /**
189
     * @param RebuildConfigEvent $event
190
     * @return void
191
     */
192
    public function onProjectConfigRebuild(RebuildConfigEvent $event)
193
    {
194
195
        Craft::$app->getProjectConfig()->remove('reasons_conditionals');
196
197
        $rows = (new Query())
198
            ->select(['reasons.uid', 'reasons.conditionals', 'fieldlayouts.uid AS fieldLayoutUid'])
199
            ->from('{{%reasons}} AS reasons')
200
            ->innerJoin('{{%fieldlayouts}} AS fieldlayouts', 'fieldlayouts.id = reasons.fieldLayoutId')
201
            ->all();
202
203
        foreach ($rows as $row) {
204
            $uid = $row['uid'];
205
            $event->config['reasons_conditionals'][$uid] = [
206
                'conditionals' => $row['conditionals'],
207
                'fieldLayoutUid' => $row['fieldLayoutUid'],
208
            ];
209
        }
210
211
        $this->clearCache();
212
    }
213
214
    /**
215
     * Clears Reasons' data caches
216
     *
217
     * @return void
218
     */
219
    public function clearCache()
220
    {
221
        Craft::$app->getCache()->delete($this->getCacheKey());
222
    }
223
224
    /**
225
     * @return array|mixed
226
     */
227
    public function getData()
228
    {
229
        $doCacheData = !Craft::$app->getConfig()->getGeneral()->devMode;
230
        $cacheKey = $this->getCacheKey();
231
232
        if ($doCacheData && $data = Craft::$app->getCache()->get($cacheKey)) {
233
            return $data;
234
        }
235
236
        $data = [
237
            'conditionals' => $this->getConditionals(),
238
            'toggleFieldTypes' => $this->getToggleFieldTypes(),
239
            'toggleFields' => $this->getToggleFields(),
240
            'fieldIds' => $this->getFieldIdsByHandle(),
241
        ];
242
243
        if ($doCacheData) {
244
            Craft::$app->getCache()->set($cacheKey, $data, self::CACHE_TTL);
245
        }
246
247
        return $data;
248
    }
249
250
    /**
251
     * @param string|array $conditionals
252
     * @return string|null
253
     */
254
    protected function prepConditionalsForProjectConfig($conditionals)
255
    {
256
        if (!$conditionals) {
257
            return null;
258
        }
259
        $return = [];
260
        $conditionals = Json::decodeIfJson($conditionals);
261
        foreach ($conditionals as $targetFieldId => $statements) {
262
            $targetFieldUid = Db::uidById('{{%fields}}', $targetFieldId);
263
            $return[$targetFieldUid] = \array_map(function (array $rules) {
264
                return \array_map(function (array $rule) {
265
                    return [
266
                        'field' => Db::uidById('{{%fields}}', $rule['fieldId']),
267
                        'compare' => $rule['compare'],
268
                        'value' => $rule['value'],
269
                    ];
270
                }, $rules);
271
            }, $statements);
272
        }
273
        return Json::encode($return);
274
    }
275
276
    /**
277
     * @param string|array $conditionals
278
     * @return array|null
279
     */
280
    protected function normalizeConditionalsFromProjectConfig($conditionals)
281
    {
282
        if (!$conditionals) {
283
            return null;
284
        }
285
        $return = [];
286
        try {
287
            $conditionals = Json::decodeIfJson($conditionals);
288
            foreach ($conditionals as $targetFieldUid => $statements) {
289
                $targetFieldId = Db::idByUid('{{%fields}}', $targetFieldUid);
290
                $return[$targetFieldId] = \array_map(function (array $rules) {
291
                    return \array_map(function (array $rule) {
292
                        return [
293
                            'fieldId' => Db::idByUid('{{%fields}}', $rule['field']),
294
                            'compare' => $rule['compare'],
295
                            'value' => $rule['value'],
296
                        ];
297
                    }, $rules);
298
                }, $statements);
299
            }
300
        } catch (\Throwable $e) {
301
            Craft::error($e->getMessage(), __METHOD__);
302
        }
303
        return $return;
304
    }
305
306
    /**
307
     * Returns all conditionals, mapped by source key
308
     *
309
     * @return array
310
     */
311
    protected function getConditionals(): array
312
    {
313
314
        // Get all conditionals from database
315
        $rows = (new Query())
316
            ->select(['reasons.id', 'reasons.fieldLayoutId', 'reasons.conditionals'])
317
            ->from('{{%reasons}} AS reasons')
318
            ->innerJoin('{{%fieldlayouts}} AS fieldlayouts', 'fieldlayouts.id = fieldLayoutId')
319
            ->all();
320
321
        // Map conditionals to field layouts, and convert field uids to ids
322
        $conditionals = [];
323
        foreach ($rows as $row) {
324
            $conditionals["fieldLayout:{$row['fieldLayoutId']}"] = $this->normalizeConditionalsFromProjectConfig($row['conditionals']);
325
        }
326
327
        // Map conditionals to sources
328
        $conditionalsBySources = [];
329
        $sources = $this->getSources();
330
        foreach ($sources as $sourceId => $fieldLayoutId) {
331
            if (!isset($conditionals["fieldLayout:{$fieldLayoutId}"])) {
332
                continue;
333
            }
334
            $conditionalsBySources[$sourceId] = $conditionals["fieldLayout:{$fieldLayoutId}"];
335
        }
336
337
        return $conditionalsBySources;
338
    }
339
340
    /**
341
     * @return array
342
     */
343
    protected function getSources(): array
344
    {
345
346
        if (!isset($this->sources)) {
347
348
            $sources = [];
349
350
            $entryTypeRecords = EntryType::find()->all();
351
            /** @var EntryType $entryTypeRecord */
352
            foreach ($entryTypeRecords as $entryTypeRecord) {
353
                $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...
354
                $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...
355
            }
356
357
            $categoryGroups = Craft::$app->getCategories()->getAllGroups();
358
            foreach ($categoryGroups as $categoryGroup) {
359
                $sources["categoryGroup:{$categoryGroup->id}"] = (int)$categoryGroup->fieldLayoutId;
360
            }
361
362
            $tagGroups = Craft::$app->getTags()->getAllTagGroups();
363
            foreach ($tagGroups as $tagGroup) {
364
                $sources["tagGroup:{$tagGroup->id}"] = (int)$tagGroup->fieldLayoutId;
365
            }
366
367
            $volumes = Craft::$app->getVolumes()->getAllVolumes();
368
            foreach ($volumes as $volume) {
369
                $sources["assetSource:{$volume->id}"] = (int)$volume->fieldLayoutId;
370
            }
371
372
            $globalSets = Craft::$app->getGlobals()->getAllSets();
373
            foreach ($globalSets as $globalSet) {
374
                $sources["globalSet:{$globalSet->id}"] = (int)$globalSet->fieldLayoutId;
375
            }
376
377
            $usersFieldLayout = Craft::$app->getFields()->getLayoutByType(User::class);
378
            $sources['users'] = $usersFieldLayout->id;
379
380
            $this->sources = $sources;
381
382
        }
383
384
        return $this->sources;
385
    }
386
387
    /**
388
     * Returns all toggleable fields
389
     *
390
     * @return array
391
     */
392
    protected function getToggleFields(): array
393
    {
394
        $toggleFieldTypes = $this->getToggleFieldTypes();
395
        $toggleFields = [];
396
        $fields = $this->getAllFields();
397
        /** @var FieldInterface $field */
398
        foreach ($fields as $field) {
399
            $fieldType = \get_class($field);
400
            if (!\in_array($fieldType, $toggleFieldTypes)) {
401
                continue;
402
            }
403
            $toggleFields[] = [
404
                'id' => (int)$field->id,
405
                'handle' => $field->handle,
406
                'name' => $field->name,
407
                'type' => $fieldType,
408
                'settings' => $field->getSettings(),
409
            ];
410
        }
411
        return $toggleFields;
412
    }
413
414
    /**
415
     * Returns all toggleable fieldtype classnames
416
     *
417
     * @return string[]
418
     */
419
    protected function getToggleFieldTypes(): array
420
    {
421
        return [
422
            Lightswitch::class,
423
            Dropdown::class,
424
            Checkboxes::class,
425
            MultiSelect::class,
426
            RadioButtons::class,
427
            Number::class,
428
            PlainText::class,
429
            Entries::class,
430
            Categories::class,
431
            Tags::class,
432
            Assets::class,
433
            Users::class,
434
        ];
435
    }
436
437
    /**
438
     * Returns all global field IDs, indexed by handle
439
     *
440
     * @return array
441
     */
442
    protected function getFieldIdsByHandle(): array
443
    {
444
        $handles = [];
445
        $fields = $this->getAllFields();
446
        foreach ($fields as $field) {
447
            $handles[$field->handle] = (int)$field->id;
448
        }
449
        return $handles;
450
    }
451
452
    /**
453
     * @return FieldInterface[]
454
     */
455
    protected function getAllFields(): array
456
    {
457
        if (!isset($this->allFields)) {
458
            $this->allFields = Craft::$app->getFields()->getAllFields('global');
459
        }
460
        return $this->allFields;
461
    }
462
463
    /**
464
     * @return string
465
     */
466
    protected function getCacheKey(): string
467
    {
468
        $reasons = Reasons::getInstance();
469
        return \implode('-', [
470
            $reasons->getHandle(),
471
            $reasons->getVersion(),
472
            $reasons->schemaVersion
473
        ]);
474
    }
475
}
476