Completed
Push — master ( 1e9bd3...f3f022 )
by Nate
04:03
created

Configuration::getFieldInfoForInput()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 56

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 56
ccs 0
cts 40
cp 0
rs 8.9599
c 0
b 0
f 0
cc 4
nc 8
nop 2
crap 20

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * @copyright  Copyright (c) Flipbox Digital Limited
5
 * @license    https://flipboxfactory.com/software/meta/license
6
 * @link       https://www.flipboxfactory.com/software/meta/
7
 */
8
9
namespace flipbox\meta\services;
10
11
use Craft;
12
use craft\base\ElementInterface;
13
use craft\base\Field;
14
use craft\base\FieldInterface;
15
use craft\fields\Matrix;
16
use craft\helpers\ArrayHelper;
17
use craft\helpers\Json;
18
use craft\helpers\StringHelper;
19
use craft\models\FieldLayoutTab;
20
use craft\records\Field as FieldRecord;
21
use flipbox\meta\db\MetaQuery;
22
use flipbox\meta\elements\Meta;
23
use flipbox\meta\fields\Meta as MetaField;
24
use flipbox\meta\helpers\Field as FieldHelper;
25
use flipbox\meta\migrations\ContentTable;
26
use flipbox\meta\records\Meta as MetaRecord;
27
use flipbox\meta\web\assets\input\Input;
28
use flipbox\meta\web\assets\settings\Settings as MetaSettingsAsset;
29
use yii\base\Component;
30
use yii\base\Exception;
31
32
/**
33
 * @author Flipbox Factory <[email protected]>
34
 * @since 1.0.0
35
 */
36
class Configuration extends Component
37
{
38
    /**
39
     * @param MetaField $metaField
40
     * @return bool
41
     * @throws \Exception
42
     * @throws \Throwable
43
     * @throws \yii\db\Exception
44
     */
45
    public function save(MetaField $metaField)
46
    {
47
        $transaction = Craft::$app->getDb()->beginTransaction();
48
        try {
49
            $contentService = Craft::$app->getContent();
50
51
            // Create/Rename table
52
            $this->ensureTable($metaField);
53
54
            // Get the originals
55
            $originalContentTable = $contentService->contentTable;
56
            $originalFieldContext = $contentService->fieldContext;
57
            $originalFieldColumnPrefix = $contentService->fieldColumnPrefix;
58
59
            // Set our content table
60
            $contentService->contentTable = FieldHelper::getContentTableName($metaField->id);
61
            $contentService->fieldContext = FieldHelper::getContextById($metaField->id);
62
            $contentService->fieldColumnPrefix = 'field_';
63
64
            // Delete old fields
65
            $this->deleteOldFields($metaField);
66
67
            // Save fields
68
            $this->saveNewFields($metaField);
69
70
            // Revert to originals
71
            $contentService->contentTable = $originalContentTable;
72
            $contentService->fieldContext = $originalFieldContext;
73
            $contentService->fieldColumnPrefix = $originalFieldColumnPrefix;
74
75
            // Save the fieldLayoutId via settings
76
            /** @var FieldRecord $fieldRecord */
77
            $fieldRecord = FieldRecord::findOne($metaField->id);
78
            $fieldRecord->settings = $metaField->getSettings();
79
80
            if (!$fieldRecord->save(true, ['settings'])) {
81
                $metaField->addError(
82
                    'settings',
83
                    Craft::t('meta', 'Unable to save settings.')
84
                );
85
                $transaction->rollback();
86
                return false;
87
            }
88
        } catch (\Exception $e) {
89
            $transaction->rollback();
90
            throw $e;
91
        }
92
93
        $transaction->commit();
94
        return true;
95
    }
96
97
    /**
98
     * @param MetaField $field
99
     * @return bool
100
     * @throws \Exception
101
     * @throws \yii\db\Exception
102
     */
103
    public function beforeDelete(MetaField $field)
104
    {
105
        $transaction = Craft::$app->getDb()->beginTransaction();
106
        try {
107
            // Delete field layout
108
            Craft::$app->getFields()->deleteLayoutById($field->fieldLayoutId);
109
110
            // Get content table name
111
            $contentTableName = FieldHelper::getContentTableName($field->id);
112
113
            // Drop the content table
114
            Craft::$app->getDb()->createCommand()
115
                ->dropTableIfExists($contentTableName)
116
                ->execute();
117
118
            // find any of the context fields
119
            $subFieldRecords = FieldRecord::find()
120
                ->andWhere(['like', 'context', MetaRecord::tableAlias() . ':%', false])
121
                ->all();
122
123
            // Delete them
124
            /** @var MetaRecord $subFieldRecord */
125
            foreach ($subFieldRecords as $subFieldRecord) {
126
                Craft::$app->getFields()->deleteFieldById($subFieldRecord->id);
127
            }
128
129
            $transaction->commit();
130
            return true;
131
        } catch (\Exception $e) {
132
            // Revert
133
            $transaction->rollback();
134
135
            throw $e;
136
        }
137
    }
138
139
140
    /**
141
     * @param MetaField $metaField
142
     * @return bool
143
     */
144
    public function validate(MetaField $metaField): bool
145
    {
146
        $validates = true;
147
148
        // Can't validate multiple new rows at once so we'll need to give these temporary context to avoid false unique
149
        // handle validation errors, and just validate those manually. Also apply the future fieldColumnPrefix so that
150
        // field handle validation takes its length into account.
151
        $contentService = Craft::$app->getContent();
152
        $originalFieldContext = $contentService->fieldContext;
153
154
        $contentService->fieldContext = StringHelper::randomString(10);
155
156
        /** @var FieldRecord $field */
157
        foreach ($metaField->getFieldLayout()->getFields() as $field) {
158
            // Hack to allow blank field names
159
            if (!$field->name) {
160
                $field->name = '__blank__';
161
            }
162
163
            if (!$field->validate()) {
164
                $metaField->hasFieldErrors = true;
165
                $validates = false;
166
            }
167
        }
168
169
        $contentService->fieldContext = $originalFieldContext;
170
171
        return $validates;
172
    }
173
174
    /**
175
     * @inheritdoc
176
     */
177
    private function createContentTable($tableName)
178
    {
179
        $migration = new ContentTable([
180
            'tableName' => $tableName
181
        ]);
182
183
        ob_start();
184
        $migration->up();
185
        ob_end_clean();
186
    }
187
188
189
    /*******************************************
190
     * HTML
191
     *******************************************/
192
193
    /**
194
     * @param MetaField $field
195
     * @param $value
196
     * @param ElementInterface|null $element
197
     * @return string
198
     * @throws Exception
199
     * @throws \Twig_Error_Loader
200
     * @throws \yii\base\InvalidConfigException
201
     */
202
    public function getInputHtml(MetaField $field, $value, ElementInterface $element = null): string
203
    {
204
        $id = Craft::$app->getView()->formatInputId($field->handle);
205
206
        // Get the field data
207
        $fieldInfo = $this->getFieldInfoForInput($field, $element);
208
209
        Craft::$app->getView()->registerAssetBundle(Input::class);
210
211
        Craft::$app->getView()->registerJs(
212
            'new Craft.MetaInput(' .
213
            '"' . Craft::$app->getView()->namespaceInputId($id) . '", ' .
214
            Json::encode($fieldInfo, JSON_UNESCAPED_UNICODE) . ', ' .
215
            '"' . Craft::$app->getView()->namespaceInputName($field->handle) . '", ' .
216
            ($field->min ?: 'null') . ', ' .
217
            ($field->max ?: 'null') .
218
            ');'
219
        );
220
221
        Craft::$app->getView()->registerTranslations('meta', [
222
            'Add new',
223
            'Add new above'
224
        ]);
225
226
        if ($value instanceof MetaQuery) {
227
            $value
228
                ->limit(null)
229
                ->status(null)
230
                ->enabledForSite(false);
231
        }
232
233
        return Craft::$app->getView()->renderTemplate(
234
            FieldHelper::TEMPLATE_PATH . '/input',
235
            [
236
                'id' => $id,
237
                'name' => $field->handle,
238
                'field' => $field,
239
                'elements' => $value->all(),
240
                'static' => false,
241
                'template' => $field::DEFAULT_TEMPLATE
242
            ]
243
        );
244
    }
245
246
    /**
247
     * @inheritdoc
248
     */
249
    public function getSettingsHtml(MetaField $field)
250
    {
251
        // Get the available field types data
252
        $fieldTypeInfo = $this->getFieldOptionsForConfiguration();
253
254
        $view = Craft::$app->getView();
255
256
        $view->registerAssetBundle(MetaSettingsAsset::class);
257
        $view->registerJs(
258
            'new Craft.MetaConfiguration(' .
259
            Json::encode($fieldTypeInfo, JSON_UNESCAPED_UNICODE) . ', ' .
260
            Json::encode(Craft::$app->getView()->getNamespace(), JSON_UNESCAPED_UNICODE) .
261
            ');'
262
        );
263
264
        $view->registerTranslations('meta', [
265
            'New field'
266
        ]);
267
268
        $fieldTypeOptions = [];
269
270
        /** @var Field|string $class */
271
        foreach (Craft::$app->getFields()->getAllFieldTypes() as $class) {
272
            $fieldTypeOptions[] = [
273
                'value' => $class,
274
                'label' => $class::displayName()
275
            ];
276
        }
277
278
//        // Handle missing fields
279
//        $fields = $field->getFields();
280
//        foreach ($fields as $i => $field) {
281
//            if ($field instanceof MissingField) {
282
//                $fields[$i] = $field->createFallback(PlainText::class);
283
//                $fields[$i]->addError('type', Craft::t('app', 'The field type “{type}” could not be found.', [
284
//                    'type' => $field->expectedType
285
//                ]));
286
//                $field->hasFieldErrors = true;
287
//            }
288
//        }
289
//        $field->setFields($fields);
290
291
        return Craft::$app->getView()->renderTemplate(
292
            FieldHelper::TEMPLATE_PATH . '/settings',
293
            [
294
                'field' => $field,
295
                'fieldTypes' => $fieldTypeOptions,
296
                'defaultTemplate' => $field::DEFAULT_TEMPLATE
297
            ]
298
        );
299
    }
300
301
    /**
302
     * TODO - eliminate this and render configuration via ajax call
303
     *
304
     * Returns html for all associated field types for the Meta field input.
305
     *
306
     * @return array
307
     */
308
    private function getFieldInfoForInput(MetaField $field, ElementInterface $element = null): array
309
    {
310
        // Set a temporary namespace for these
311
        $originalNamespace = Craft::$app->getView()->getNamespace();
312
        $namespace = Craft::$app->getView()->namespaceInputName(
313
            $field->handle . '[__META__][fields]',
314
            $originalNamespace
315
        );
316
        Craft::$app->getView()->setNamespace($namespace);
317
318
        // Create a fake meta so the field types have a way to get at the owner element, if there is one
319
        $meta = new Meta();
320
        $meta->fieldId = $field->id;
0 ignored issues
show
Documentation Bug introduced by
It seems like $field->id can also be of type string. However, the property $fieldId is declared as type integer|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
321
322
        if ($element) {
323
            $meta->setOwner($element);
324
            $meta->siteId = $element->siteId;
325
        }
326
327
        $fieldLayoutFields = $field->getFieldLayout()->getFields();
328
329
        // Set $_isFresh's
330
        foreach ($fieldLayoutFields as $field) {
331
            $field->setIsFresh(true);
332
        }
333
334
        Craft::$app->getView()->startJsBuffer();
335
336
        $bodyHtml = Craft::$app->getView()->namespaceInputs(
337
            Craft::$app->getView()->renderTemplate(
338
                '_includes/fields',
339
                [
340
                    'namespace' => null,
341
                    'fields' => $fieldLayoutFields,
342
                    'element' => $meta
343
                ]
344
            )
345
        );
346
347
        // Reset $_isFresh's
348
        foreach ($fieldLayoutFields as $field) {
349
            $field->setIsFresh(null);
350
        }
351
352
        $footHtml = Craft::$app->getView()->clearJsBuffer();
353
354
        $fields = [
355
            'bodyHtml' => $bodyHtml,
356
            'footHtml' => $footHtml,
357
        ];
358
359
        // Revert namespace
360
        Craft::$app->getView()->setNamespace($originalNamespace);
361
362
        return $fields;
363
    }
364
365
    /**
366
     *
367
     * TODO - eliminate this and render configuration via ajax call
368
     *
369
     * Returns info about each field type for the configurator.
370
     *
371
     * @return array
372
     */
373
    private function getFieldOptionsForConfiguration()
374
    {
375
        $disallowedFields = [
376
            MetaField::class,
377
            Matrix::class
378
        ];
379
380
        $fieldTypes = [];
381
382
        // Set a temporary namespace for these
383
        $originalNamespace = Craft::$app->getView()->getNamespace();
384
        $namespace = Craft::$app->getView()->namespaceInputName('fields[__META_FIELD__][settings]', $originalNamespace);
385
        Craft::$app->getView()->setNamespace($namespace);
386
387
        /** @var Field|string $class */
388
        foreach (Craft::$app->getFields()->getAllFieldTypes() as $class) {
389
            // Ignore disallowed fields
390
            if (in_array($class, $disallowedFields)) {
391
                continue;
392
            }
393
394
            Craft::$app->getView()->startJsBuffer();
395
396
            /** @var FieldInterface $field */
397
            $field = new $class();
398
399
            if ($settingsHtml = (string)$field->getSettingsHtml()) {
400
                $settingsHtml = Craft::$app->getView()->namespaceInputs($settingsHtml);
401
            }
402
403
            $settingsBodyHtml = $settingsHtml;
404
            $settingsFootHtml = Craft::$app->getView()->clearJsBuffer();
405
406
            $fieldTypes[] = [
407
                'type' => $class,
408
                'name' => $class::displayName(),
409
                'settingsBodyHtml' => $settingsBodyHtml,
410
                'settingsFootHtml' => $settingsFootHtml,
411
            ];
412
        }
413
414
        Craft::$app->getView()->setNamespace($originalNamespace);
415
416
        return $fieldTypes;
417
    }
418
419
    /**
420
     * @param MetaField $metaField
421
     * @throws Exception
422
     * @throws \Throwable
423
     */
424
    private function deleteOldFields(MetaField $metaField)
425
    {
426
        /** @var \craft\services\Fields $fieldsService */
427
        $fieldsService = Craft::$app->getFields();
428
429
        // Get existing fields
430
        $oldFields = $fieldsService->getAllFields(FieldHelper::getContextById($metaField->id));
431
        $oldFieldsById = ArrayHelper::index($oldFields, 'id');
432
433
        /** @var \craft\base\Field $field */
434
        foreach ($metaField->getFieldLayout()->getFields() as $field) {
435
            if (!$field->getIsNew()) {
436
                ArrayHelper::remove($oldFieldsById, $field->id);
437
            }
438
        }
439
440
        // Drop the old fields
441
        foreach ($oldFieldsById as $field) {
442
            if (!$fieldsService->deleteField($field)) {
443
                throw new Exception(Craft::t('app', 'An error occurred while deleting this Meta field.'));
444
            }
445
        }
446
447
        // Refresh the schema cache
448
        Craft::$app->getDb()->getSchema()->refresh();
449
    }
450
451
    /**
452
     * @param MetaField $metaField
453
     * @throws Exception
454
     * @throws \Throwable
455
     */
456
    private function saveNewFields(MetaField $metaField)
457
    {
458
        $fieldLayoutFields = [];
459
        $sortOrder = 0;
460
461
        /** @var \craft\services\Fields $fieldsService */
462
        $fieldsService = Craft::$app->getFields();
463
464
        // Save field
465
        /** @var \craft\base\Field $field */
466
        foreach ($metaField->getFieldLayout()->getFields() as $field) {
467
            // Save field (we validated earlier)
468
            if (!$fieldsService->saveField($field, false)) {
469
                throw new Exception('An error occurred while saving this Meta field.');
470
            }
471
472
            // Set sort order
473
            $field->sortOrder = ++$sortOrder;
474
475
            $fieldLayoutFields[] = $field;
476
        }
477
478
        $fieldLayout = $metaField->getFieldLayout();
479
480
        $fieldLayoutTab = new FieldLayoutTab([
481
            'name' => 'Fields',
482
            'sortOrder' => 1,
483
            'fields' => $fieldLayoutFields
484
        ]);
485
486
        $fieldLayout->setTabs([$fieldLayoutTab]);
487
        $fieldLayout->setFields($fieldLayoutFields);
488
489
        $fieldsService->saveLayout($fieldLayout);
490
491
        // Update the element & record with our new field layout ID
492
        $metaField->setFieldLayout($fieldLayout);
493
        $metaField->fieldLayoutId = (int)$fieldLayout->id;
494
    }
495
496
    /**
497
     * @param MetaField $metaField
498
     */
499
    private function ensureTable(MetaField $metaField)
500
    {
501
        // Create the content table first since the element fields will need it
502
        $contentTable = FieldHelper::getContentTableName($metaField->id);
503
504
        // Do we need to create/rename the content table?
505
        if (!Craft::$app->getDb()->tableExists($contentTable)) {
506
            $this->createContentTable($contentTable);
507
            Craft::$app->getDb()->getSchema()->refresh();
508
        }
509
    }
510
}
511