Passed
Pull Request — master (#93)
by
unknown
08:22
created

BlockTypes::deleteByContext()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 6
c 3
b 0
f 0
dl 0
loc 13
rs 10
cc 3
nc 3
nop 2
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\models\BlockType;
14
use angellco\spoon\records\BlockType as BlockTypeRecord;
15
use angellco\spoon\errors\BlockTypeNotFoundException;
16
17
use Craft;
18
use craft\base\Component;
19
use craft\base\Field;
20
use craft\db\Table;
21
use craft\events\ConfigEvent;
22
use craft\helpers\Db;
23
use craft\helpers\ProjectConfig as ProjectConfigHelper;
24
use craft\helpers\StringHelper;
25
use craft\models\FieldLayout;
26
27
/**
28
 * BlockTypes Service
29
 *
30
 * @author    Angell & Co
31
 * @package   Spoon
32
 * @since     3.0.0
33
 */
34
class BlockTypes extends Component
35
{
36
    // Private Properties
37
    // =========================================================================
38
39
    private $_blockTypesByContext;
40
41
    private $_superTablePlugin;
42
43
    private $_superTableService;
44
45
    const CONFIG_BLOCKTYPE_KEY = 'spoonBlockTypes';
46
47
    // Public Methods
48
    // =========================================================================
49
50
    /**
51
     * Returns a Spoon block type model by its ID
52
     *
53
     * @param $id
54
     *
55
     * @return BlockType|null
56
     * @throws BlockTypeNotFoundException
57
     */
58
    public function getById($id)
59
    {
60
        $blockTypeRecord = BlockTypeRecord::findOne($id);
61
62
        if (!$blockTypeRecord) {
0 ignored issues
show
introduced by
$blockTypeRecord is of type yii\db\ActiveRecord, thus it always evaluated to true.
Loading history...
63
            throw new BlockTypeNotFoundException(Craft::t('spoon', 'No Spoon block type exists with the ID “{id}”', ['id' => $id]));
64
        }
65
66
        return $this->_populateBlockTypeFromRecord($blockTypeRecord);
67
    }
68
69
    /**
70
     * Returns a single BlockType Model by its context and blockTypeId
71
     *
72
     * @param bool $context
73
     * @param bool $matrixBlockTypeId
74
     *
75
     * @return BlockType|bool|null
76
     */
77
    public function getBlockType($context = false, $matrixBlockTypeId = false)
78
    {
79
80
        if (!$context || !$matrixBlockTypeId)
81
        {
82
            return false;
83
        }
84
85
        $blockTypeRecord = BlockTypeRecord::findOne([
86
            'context'           => $context,
87
            'matrixBlockTypeId' => $matrixBlockTypeId
88
        ]);
89
90
        if (!$blockTypeRecord) {
0 ignored issues
show
introduced by
$blockTypeRecord is of type yii\db\ActiveRecord, thus it always evaluated to true.
Loading history...
91
            return null;
92
        }
93
94
        return $this->_populateBlockTypeFromRecord($blockTypeRecord);
95
96
    }
97
98
    /**
99
     * Returns a block type by its context.
100
     *
101
     * @param string       $context
102
     * @param null|string  $groupBy Group by an optional model attribute to group by
103
     * @param bool         $ignoreSubContext Optionally ignore the sub context (id)
104
     * @param null|integer $fieldId Optionally filter by fieldId
105
     *
106
     * @return array
107
     */
108
    public function getByContext($context, $groupBy = null, $ignoreSubContext = false, $fieldId = null): array
109
    {
110
111
        if ($ignoreSubContext) {
112
113
            if ($fieldId !== null) {
114
                if ($context === 'global') {
115
                    $condition = [
116
                        'fieldId' => $fieldId,
117
                        'context' => 'global'
118
                    ];
119
                } else {
120
                    $condition = [
121
                        'fieldId' => $fieldId,
122
                        ['like', 'context', $context.'%', false]
123
                    ];
124
                }
125
            } else {
126
                if ($context === 'global') {
127
                    $condition = [
128
                        'context' => 'global'
129
                    ];
130
                } else {
131
                    $condition = ['like', 'context', $context.'%', false];
132
                }
133
            }
134
135
        } else {
136
            $condition = [
137
                'context' => $context
138
            ];
139
140
            if ($fieldId !== null)
141
            {
142
                $condition['fieldId'] = $fieldId;
143
            }
144
        }
145
146
        $blockTypeRecords = BlockTypeRecord::find()
147
            ->where($condition)
148
            ->orderBy(['groupSortOrder' => SORT_ASC, 'sortOrder' => SORT_ASC])
149
            ->all();
150
151
        if ($blockTypeRecords) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $blockTypeRecords of type yii\db\ActiveRecordInterface[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
152
153
            foreach ($blockTypeRecords as $blockTypeRecord) {
154
                $blockType = $this->_populateBlockTypeFromRecord($blockTypeRecord);
155
                $this->_blockTypesByContext[$context][$blockType->id] = $blockType;
156
            }
157
158
        } else {
159
            return [];
160
        }
161
162
        if ($groupBy !== null) {
163
            $return = [];
164
165
            foreach ($this->_blockTypesByContext[$context] as $blockType)
166
            {
167
                $return[$blockType->$groupBy][] = $blockType;
168
            }
169
170
            return $return;
171
        }
172
173
        return $this->_blockTypesByContext[$context];
174
175
    }
176
177
    /**
178
     * Saves our version of a block type into the project config
179
     *
180
     * @param BlockType $blockType
181
     *
182
     * @return bool
183
     * @throws \Exception
184
     */
185
    public function save(BlockType $blockType): bool
186
    {
187
        $isNew = !$blockType->id;
188
189
        // Ensure the block type has a UID
190
        if ($isNew) {
191
            $blockType->uid = StringHelper::UUID();
192
        } else if (!$blockType->uid) {
193
            $existingBlockTypeRecord = BlockTypeRecord::findOne($blockType->id);
194
195
            if (!$existingBlockTypeRecord) {
0 ignored issues
show
introduced by
$existingBlockTypeRecord is of type yii\db\ActiveRecord, thus it always evaluated to true.
Loading history...
196
                throw new BlockTypeNotFoundException("No Spoon Block Type exists with the ID “{$blockType->id}”");
197
            }
198
199
            $blockType->uid = $existingBlockTypeRecord->uid;
200
        }
201
202
        // Make sure it validates
203
        if (!$blockType->validate()) {
204
            return false;
205
        }
206
207
        // Save it to the project config
208
        $configData = [
209
            'groupName' => $blockType->groupName,
210
            'groupSortOrder' => $blockType->groupSortOrder,
211
            'sortOrder' => $blockType->sortOrder,
212
            'context' => $blockType->context,
213
            '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...
214
            'matrixBlockType' => $blockType->getBlockType()->uid,
215
        ];
216
217
        // Handle any currently attached field layouts
218
        /** @var FieldLayout $fieldLayout */
219
        $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

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

480
        $matrixField = Craft::$app->fields->getFieldById(/** @scrutinizer ignore-type */ $blockType->fieldId);
Loading history...
481
        if (!$matrixField) {
0 ignored issues
show
introduced by
$matrixField is of type craft\base\Field, thus it always evaluated to true.
Loading history...
482
            return null;
483
        }
484
        $blockType->fieldHandle = $matrixField->handle;
485
486
487
        // Super Table support
488
        if (!$this->_superTablePlugin) {
489
            $this->_superTablePlugin = \Craft::$app->plugins->getPluginByPackageName('verbb/super-table');
490
        }
491
        if ($this->_superTablePlugin) {
492
493
            if (!$this->_superTableService) {
494
                $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...
495
            }
496
497
            // If the field is actually inside a SuperTable
498
            if (strpos($matrixField->context, 'superTableBlockType') === 0) {
499
                $parts = explode(':', $matrixField->context);
500
                if (isset($parts[1])) {
501
502
                    $superTableBlockTypeId = Db::idByUid('{{%supertableblocktypes}}', $parts[1]);
503
504
                    /** @var \verbb\supertable\models\SuperTableBlockTypeModel $superTableBlockType */
505
                    $superTableBlockType = $this->_superTableService->getBlockTypeById($superTableBlockTypeId);
506
507
                    /** @var \verbb\supertable\fields\SuperTableField $superTableField */
508
                    $superTableField = \Craft::$app->fields->getFieldById($superTableBlockType->fieldId);
509
510
                    $blockType->fieldHandle = $superTableField->handle."-".$matrixField->handle;
511
512
                    // If the context of _this_ field is inside a Matrix block ... then we need to do more inception
513
                    if (strpos($superTableField->context, 'matrixBlockType') === 0) {
514
                        $nestedParts = explode(':', $superTableField->context);
515
                        if (isset($nestedParts[1])) {
516
517
                            $matrixBlockTypeId = Db::idByUid('{{%matrixblocktypes}}', $nestedParts[1]);
518
519
                            /** @var craft\models\MatrixBlockType $matrixBlockType */
520
                            $matrixBlockType = \Craft::$app->matrix->getBlockTypeById($matrixBlockTypeId);
521
522
                            /** @var craft\fields\Matrix $globalField */
523
                            $globalField = \Craft::$app->fields->getFieldById($matrixBlockType->fieldId);
524
525
                            $blockType->fieldHandle = $globalField->handle."-".$superTableField->handle."-".$matrixField->handle;
526
                        }
527
                    }
528
                }
529
            }
530
        }
531
532
533
        // Save the MatrixBlockTypeModel on to our model
534
        $blockType->matrixBlockType = $blockType->getBlockType();
535
536
        // Save the field layout content on to our model
537
        if ($blockType->fieldLayoutId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $blockType->fieldLayoutId of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
538
            $layout = $blockType->getFieldLayout();
539
            $blockType->fieldLayoutModel = [
540
                'tabs' => $layout->getTabs(),
541
                'fields' => $layout->getFields()
542
            ];
543
        }
544
545
        return $blockType;
546
    }
547
548
}
549