Passed
Push — master ( 1ca40c...54695a )
by Josh
07:44 queued 11s
created

BlockTypes   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 506
Duplicated Lines 0 %

Importance

Changes 25
Bugs 0 Features 0
Metric Value
wmc 50
eloc 194
c 25
b 0
f 0
dl 0
loc 506
rs 8.4

12 Methods

Rating   Name   Duplication   Size   Complexity  
A getById() 0 9 2
A __construct() 0 4 1
B _populateBlockTypeFromRecord() 0 78 9
A getFieldLayoutIds() 0 19 3
A getBlockType() 0 18 4
A saveFieldLayout() 0 22 2
A deleteByContext() 0 13 3
A handleChangedBlockType() 0 53 4
B save() 0 55 8
A handleDeletedBlockType() 0 21 3
B getByContext() 0 65 10
A _getBlockTypeRecord() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like BlockTypes 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 BlockTypes, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Spoon plugin for Craft CMS 3.x
4
 *
5
 * Enhance Matrix
6
 *
7
 * @link      https://angell.io
8
 * @copyright Copyright (c) 2018 Angell & Co
9
 */
10
11
namespace angellco\spoon\services;
12
13
use angellco\spoon\Spoon;
14
use angellco\spoon\models\BlockType;
15
use angellco\spoon\records\BlockType as BlockTypeRecord;
16
use angellco\spoon\errors\BlockTypeNotFoundException;
17
18
use Craft;
19
use craft\base\Component;
20
use craft\base\Field;
21
use craft\db\Table;
22
use craft\events\ConfigEvent;
23
use craft\helpers\Db;
24
use craft\helpers\ProjectConfig as ProjectConfigHelper;
25
use craft\helpers\StringHelper;
26
use craft\models\FieldLayout;
27
28
/**
29
 * BlockTypes Service
30
 *
31
 * @author    Angell & Co
32
 * @package   Spoon
33
 * @since     3.0.0
34
 */
35
class BlockTypes extends Component
36
{
37
    // Private Properties
38
    // =========================================================================
39
40
    private $_blockTypesByContext;
41
42
    private $_superTablePlugin;
43
44
    private $_superTableService;
45
46
    const CONFIG_BLOCKTYPE_KEY = 'spoonBlockTypes';
47
48
    // Public Methods
49
    // =========================================================================
50
51
    /**
52
     * BlockTypes constructor.
53
     *
54
     * @param array $config
55
     */
56
    public function __construct($config = []) {
57
        parent::__construct($config);
58
        // Refresh fields cache in case something has gone awry
59
        Craft::$app->fields->refreshFields();
60
    }
61
62
    /**
63
     * Returns a Spoon block type model by its ID
64
     *
65
     * @param $id
66
     *
67
     * @return BlockType|null
68
     * @throws BlockTypeNotFoundException
69
     */
70
    public function getById($id): ?BlockType
71
    {
72
        $blockTypeRecord = BlockTypeRecord::findOne($id);
73
74
        if (!$blockTypeRecord) {
0 ignored issues
show
introduced by
$blockTypeRecord is of type yii\db\ActiveRecord, thus it always evaluated to true.
Loading history...
75
            throw new BlockTypeNotFoundException(Craft::t('spoon', 'No Spoon block type exists with the ID “{id}”', ['id' => $id]));
76
        }
77
78
        return $this->_populateBlockTypeFromRecord($blockTypeRecord);
79
    }
80
81
    /**
82
     * Returns a single BlockType Model by its context and blockTypeId
83
     *
84
     * @param bool $context
85
     * @param bool $matrixBlockTypeId
86
     *
87
     * @return BlockType|bool|null
88
     */
89
    public function getBlockType($context = false, $matrixBlockTypeId = false)
90
    {
91
92
        if (!$context || !$matrixBlockTypeId)
93
        {
94
            return false;
95
        }
96
97
        $blockTypeRecord = BlockTypeRecord::findOne([
98
            'context'           => $context,
99
            'matrixBlockTypeId' => $matrixBlockTypeId
100
        ]);
101
102
        if (!$blockTypeRecord) {
0 ignored issues
show
introduced by
$blockTypeRecord is of type yii\db\ActiveRecord, thus it always evaluated to true.
Loading history...
103
            return null;
104
        }
105
106
        return $this->_populateBlockTypeFromRecord($blockTypeRecord);
107
108
    }
109
110
    /**
111
     * Returns a block type by its context.
112
     *
113
     * @param string       $context
114
     * @param null|string  $groupBy Group by an optional model attribute to group by
115
     * @param bool         $ignoreSubContext Optionally ignore the sub context (id)
116
     * @param null|integer $fieldId Optionally filter by fieldId
117
     *
118
     * @return array
119
     */
120
    public function getByContext($context, $groupBy = null, $ignoreSubContext = false, $fieldId = null): array
121
    {
122
123
        if ($ignoreSubContext) {
124
125
            if ($fieldId !== null) {
126
                if ($context === 'global') {
127
                    $condition = [
128
                        'fieldId' => $fieldId,
129
                        'context' => 'global'
130
                    ];
131
                } else {
132
                    $condition = [
133
                        'fieldId' => $fieldId,
134
                        ['like', 'context', $context.'%', false]
135
                    ];
136
                }
137
            } else {
138
                if ($context === 'global') {
139
                    $condition = [
140
                        'context' => 'global'
141
                    ];
142
                } else {
143
                    $condition = ['like', 'context', $context.'%', false];
144
                }
145
            }
146
147
            $blockTypeRecords = BlockTypeRecord::find()->where($condition)->all();
148
149
        } else {
150
            $condition = [
151
                'context' => $context
152
            ];
153
154
            if ($fieldId !== null)
155
            {
156
                $condition['fieldId'] = $fieldId;
157
            }
158
159
            $blockTypeRecords = BlockTypeRecord::findAll($condition);
160
161
        }
162
163
        if ($blockTypeRecords) {
164
165
            foreach ($blockTypeRecords as $blockTypeRecord) {
166
                $blockType = $this->_populateBlockTypeFromRecord($blockTypeRecord);
167
                $this->_blockTypesByContext[$context][$blockType->id] = $blockType;
168
            }
169
170
        } else {
171
            return [];
172
        }
173
174
        if ($groupBy !== null) {
175
            $return = [];
176
177
            foreach ($this->_blockTypesByContext[$context] as $blockType)
178
            {
179
                $return[$blockType->$groupBy][] = $blockType;
180
            }
181
            return $return;
182
        }
183
184
        return $this->_blockTypesByContext[$context];
185
186
    }
187
188
    /**
189
     * Saves our version of a block type into the project config
190
     *
191
     * @param BlockType $blockType
192
     *
193
     * @return bool
194
     * @throws \Exception
195
     */
196
    public function save(BlockType $blockType): bool
197
    {
198
        $isNew = !$blockType->id;
199
200
        // Ensure the block type has a UID
201
        if ($isNew) {
202
            $blockType->uid = StringHelper::UUID();
203
        } else if (!$blockType->uid) {
204
            $existingBlockTypeRecord = BlockTypeRecord::findOne($blockType->id);
205
206
            if (!$existingBlockTypeRecord) {
0 ignored issues
show
introduced by
$existingBlockTypeRecord is of type yii\db\ActiveRecord, thus it always evaluated to true.
Loading history...
207
                throw new BlockTypeNotFoundException("No Spoon Block Type exists with the ID “{$blockType->id}”");
208
            }
209
210
            $blockType->uid = $existingBlockTypeRecord->uid;
211
        }
212
213
        // Make sure it validates
214
        if (!$blockType->validate()) {
215
            return false;
216
        }
217
218
        // Save it to the project config
219
        $configData = [
220
            'groupName' => $blockType->groupName,
221
            'context' => $blockType->context,
222
            'field' => $blockType->getField()->uid,
0 ignored issues
show
Bug introduced by
Accessing uid on the interface craft\base\FieldInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
223
            'matrixBlockType' => $blockType->getBlockType()->uid,
224
        ];
225
226
        // Handle any currently attached field layouts
227
        /** @var FieldLayout $fieldLayout */
228
        $fieldLayout = $blockType->getFieldLayout();
0 ignored issues
show
Bug introduced by
The method getFieldLayout() does not exist on angellco\spoon\models\BlockType. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

228
        /** @scrutinizer ignore-call */ 
229
        $fieldLayout = $blockType->getFieldLayout();
Loading history...
229
230
        if ($fieldLayout && $fieldLayout->uid) {
231
            $fieldLayoutConfig = $fieldLayout->getConfig();
232
233
            $layoutUid = $fieldLayout->uid;
234
235
            $configData['fieldLayout'] = [
236
                $layoutUid => $fieldLayoutConfig
237
            ];
238
        } else {
239
            $configData['fieldLayout'] = null;
240
        }
241
242
        $configPath = self::CONFIG_BLOCKTYPE_KEY . '.' . $blockType->uid;
243
244
        Craft::$app->projectConfig->set($configPath, $configData);
245
246
        if ($isNew) {
247
            $blockType->id = Db::idByUid('{{%spoon_blocktypes}}', $blockType->uid);
248
        }
249
250
        return true;
251
    }
252
253
    /**
254
     * Deletes all the block types for a given context from the project config
255
     *
256
     * @param null $context
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $context is correct as it would always require null to be passed?
Loading history...
257
     * @param null $fieldId
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $fieldId is correct as it would always require null to be passed?
Loading history...
258
     *
259
     * @return bool|null
260
     * @throws \Exception
261
     */
262
    public function deleteByContext($context = null, $fieldId = null): ?bool
263
    {
264
        if (!$context) {
0 ignored issues
show
introduced by
$context is of type null, thus it always evaluated to false.
Loading history...
265
            return false;
266
        }
267
268
        $blockTypes = $this->getByContext($context, null, false, $fieldId);
269
270
        foreach ($blockTypes as $blockType) {
271
            Craft::$app->getProjectConfig()->remove(self::CONFIG_BLOCKTYPE_KEY . '.' . $blockType->uid);
272
        }
273
274
        return true;
275
    }
276
277
    // Project config methods
278
    // =========================================================================
279
280
    /**
281
     * Handles a changed block type and saves it to the database
282
     *
283
     * @param ConfigEvent $event
284
     *
285
     * @throws \Throwable
286
     */
287
    public function handleChangedBlockType(ConfigEvent $event): void
288
    {
289
        $fieldsService = Craft::$app->getFields();
290
291
        $uid = $event->tokenMatches[0];
292
        $data = $event->newValue;
293
294
        $fieldUid = $data['field'];
295
        $fieldId = Db::idByUid(Table::FIELDS, $fieldUid);
296
297
        $matrixBlockTypeUid = $data['matrixBlockType'];
298
        $matrixBlockTypeId = Db::idByUid(Table::MATRIXBLOCKTYPES, $matrixBlockTypeUid);
299
300
        // Make sure fields and sites are processed
301
        ProjectConfigHelper::ensureAllSitesProcessed();
302
        ProjectConfigHelper::ensureAllFieldsProcessed();
303
304
        $db = Craft::$app->getDb();
305
        $transaction = $db->beginTransaction();
306
307
        try {
308
            // Get the record
309
            $blockTypeRecord = $this->_getBlockTypeRecord($uid);
310
311
            // Prep the record with the new data
312
            $blockTypeRecord->fieldId = $fieldId;
313
            $blockTypeRecord->matrixBlockTypeId = $matrixBlockTypeId;
314
            $blockTypeRecord->groupName = $data['groupName'];
315
            $blockTypeRecord->context = $data['context'];
316
            $blockTypeRecord->uid = $uid;
317
318
            // Handle the field layout
319
            if (!empty($data['fieldLayout'])) {
320
                // Save the field layout
321
                $layout = FieldLayout::createFromConfig(reset($data['fieldLayout']));
322
                $layout->id = $blockTypeRecord->fieldLayoutId;
323
                $layout->type = BlockType::class;
324
                $layout->uid = key($data['fieldLayout']);
325
                $fieldsService->saveLayout($layout);
326
                $blockTypeRecord->fieldLayoutId = $layout->id;
327
            } else if ($blockTypeRecord->fieldLayoutId) {
328
                // Delete the field layout
329
                $fieldsService->deleteLayoutById($blockTypeRecord->fieldLayoutId);
330
                $blockTypeRecord->fieldLayoutId = null;
331
            }
332
333
            // Save the record
334
            $blockTypeRecord->save(false);
335
336
            $transaction->commit();
337
        } catch (\Throwable $e) {
338
            $transaction->rollBack();
339
            throw $e;
340
        }
341
    }
342
343
    /**
344
     * Handles a deleted block type and removes it from the database
345
     *
346
     * @param ConfigEvent $event
347
     *
348
     * @throws \Throwable
349
     */
350
    public function handleDeletedBlockType(ConfigEvent $event): void
351
    {
352
        $uid = $event->tokenMatches[0];
353
        $blockTypeRecord = $this->_getBlockTypeRecord($uid);
354
355
        if (!$blockTypeRecord->id) {
356
            return;
357
        }
358
359
        $db = Craft::$app->getDb();
360
        $transaction = $db->beginTransaction();
361
        try {
362
            // Delete the block type record
363
            $db->createCommand()
364
                ->delete('{{%spoon_blocktypes}}', ['id' => $blockTypeRecord->id])
365
                ->execute();
366
367
            $transaction->commit();
368
        } catch (\Throwable $e) {
369
            $transaction->rollBack();
370
            throw $e;
371
        }
372
    }
373
374
    // Public Methods for FLDs on our Block Types
375
    // =========================================================================
376
377
    /**
378
     * Saves a field layout into the project config nested under the block type config
379
     *
380
     * @param BlockType $blockType
381
     *
382
     * @return bool
383
     * @throws \Exception
384
     */
385
    public function saveFieldLayout(BlockType $blockType): bool
386
    {
387
        /** @var FieldLayout $fieldLayout */
388
        $fieldLayout = $blockType->getFieldLayout();
389
//        $oldFieldLayoutId = $blockType->fieldLayoutId;
390
391
        if ($fieldLayout->uid) {
392
            $layoutUid = $fieldLayout->uid;
393
        } else {
394
            $layoutUid = StringHelper::UUID();
395
            $fieldLayout->uid = $layoutUid;
396
        }
397
398
        $fieldLayoutConfig = $fieldLayout->getConfig();
399
400
        $configPath = self::CONFIG_BLOCKTYPE_KEY . '.' . $blockType->uid . '.fieldLayout';
401
402
        Craft::$app->projectConfig->set($configPath, [
403
            $layoutUid => $fieldLayoutConfig
404
        ]);
405
406
        return true;
407
    }
408
409
    /**
410
     * Returns an array of fieldLayoutIds indexed by matrixBlockTypeIds
411
     * for the given context and fieldId combination
412
     *
413
     * @param  string       $context required
414
     * @param  bool|integer $fieldId required
415
     * @return false|array
416
     */
417
    public function getFieldLayoutIds($context, $fieldId = false)
418
    {
419
420
        if (!$fieldId)
421
        {
422
            return false;
423
        }
424
425
        $blockTypeRecords = BlockTypeRecord::findAll([
426
            'context' => $context,
427
            'fieldId' => $fieldId
428
        ]);
429
430
        $return = array();
431
        foreach ($blockTypeRecords as $blockTypeRecord)
432
        {
433
            $return[$blockTypeRecord->matrixBlockTypeId] = $blockTypeRecord->fieldLayoutId;
434
        }
435
        return $return;
436
437
    }
438
439
440
    // Private Methods
441
    // =========================================================================
442
443
    /**
444
     * Gets a block type's record by uid.
445
     *
446
     * @param string $uid
447
     *
448
     * @return BlockTypeRecord
449
     */
450
    private function _getBlockTypeRecord(string $uid): BlockTypeRecord
451
    {
452
        $record = BlockTypeRecord::findOne(['uid' => $uid]);
453
        return $record ?? new BlockTypeRecord();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $record ?? new an...oon\records\BlockType() could return the type yii\db\ActiveRecord which includes types incompatible with the type-hinted return angellco\spoon\records\BlockType. Consider adding an additional type-check to rule them out.
Loading history...
454
    }
455
456
    /**
457
     * Populates a BlockTypeModel with attributes from a BlockTypeRecord.
458
     *
459
     * @param BlockTypeRecord $blockTypeRecord
460
     *
461
     * @return BlockType|null
462
     */
463
    private function _populateBlockTypeFromRecord(BlockTypeRecord $blockTypeRecord): ?BlockType
464
    {
465
        $blockType = new BlockType($blockTypeRecord->toArray([
466
            'id',
467
            'uid',
468
            'fieldId',
469
            'matrixBlockTypeId',
470
            'fieldLayoutId',
471
            'groupName',
472
            'context'
473
        ]));
474
        
475
        // Use the fieldId to get the field and save the handle on to the model
476
        /** @var Field $matrixField */
477
        $matrixField = Craft::$app->fields->getFieldById($blockType->fieldId);
0 ignored issues
show
Bug introduced by
It seems like $blockType->fieldId can also be of type null; however, parameter $fieldId of craft\services\Fields::getFieldById() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

477
        $matrixField = Craft::$app->fields->getFieldById(/** @scrutinizer ignore-type */ $blockType->fieldId);
Loading history...
478
        if (!$matrixField) {
0 ignored issues
show
introduced by
$matrixField is of type craft\base\Field, thus it always evaluated to true.
Loading history...
479
            return null;
480
        }
481
        $blockType->fieldHandle = $matrixField->handle;
482
483
484
        // Super Table support
485
        if (!$this->_superTablePlugin) {
486
            $this->_superTablePlugin = \Craft::$app->plugins->getPluginByPackageName('verbb/super-table');
487
        }
488
        if ($this->_superTablePlugin) {
489
490
            if (!$this->_superTableService) {
491
                $this->_superTableService = new \verbb\supertable\services\SuperTableService();
0 ignored issues
show
Bug introduced by
The type verbb\supertable\services\SuperTableService was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
492
            }
493
494
            // If the field is actually inside a SuperTable
495
            if (strpos($matrixField->context, 'superTableBlockType') === 0) {
496
                $parts = explode(':', $matrixField->context);
497
                if (isset($parts[1])) {
498
499
                    $superTableBlockTypeId = Db::idByUid('{{%supertableblocktypes}}', $parts[1]);
500
501
                    /** @var \verbb\supertable\models\SuperTableBlockTypeModel $superTableBlockType */
502
                    $superTableBlockType = $this->_superTableService->getBlockTypeById($superTableBlockTypeId);
503
504
                    /** @var \verbb\supertable\fields\SuperTableField $superTableField */
505
                    $superTableField = \Craft::$app->fields->getFieldById($superTableBlockType->fieldId);
506
507
                    $blockType->fieldHandle = $superTableField->handle."-".$matrixField->handle;
508
509
                    // If the context of _this_ field is inside a Matrix block ... then we need to do more inception
510
                    if (strpos($superTableField->context, 'matrixBlockType') === 0) {
511
                        $nestedParts = explode(':', $superTableField->context);
512
                        if (isset($nestedParts[1])) {
513
514
                            $matrixBlockTypeId = Db::idByUid('{{%matrixblocktypes}}', $nestedParts[1]);
515
516
                            /** @var craft\models\MatrixBlockType $matrixBlockType */
517
                            $matrixBlockType = \Craft::$app->matrix->getBlockTypeById($matrixBlockTypeId);
518
519
                            /** @var craft\fields\Matrix $globalField */
520
                            $globalField = \Craft::$app->fields->getFieldById($matrixBlockType->fieldId);
521
522
                            $blockType->fieldHandle = $globalField->handle."-".$superTableField->handle."-".$matrixField->handle;
523
                        }
524
                    }
525
                }
526
            }
527
        }
528
529
530
        // Save the MatrixBlockTypeModel on to our model
531
        $blockType->matrixBlockType = $blockType->getBlockType();
532
533
        // Save the field layout content on to our model
534
        $layout = $blockType->getFieldLayout();
535
        $blockType->fieldLayoutModel = [
536
            'tabs'   => $layout->getTabs(),
537
            'fields' => $layout->getFields()
538
        ];
539
540
        return $blockType;
541
    }
542
543
}
544