Passed
Push — develop ( c2d73a...d5bcac )
by M. Mikkel
03:33
created

ReasonsService::getConditionals()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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