Configuration::getFieldOptionsForConfiguration()   B
last analyzed

Complexity

Conditions 5
Paths 8

Size

Total Lines 49

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
dl 0
loc 49
ccs 0
cts 34
cp 0
rs 8.8016
c 0
b 0
f 0
cc 5
nc 8
nop 0
crap 30
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\FieldLayout;
20
use craft\models\FieldLayoutTab;
21
use craft\records\Field as FieldRecord;
22
use flipbox\meta\db\MetaQuery;
23
use flipbox\meta\elements\Meta;
24
use flipbox\meta\fields\Meta as MetaField;
25
use flipbox\meta\helpers\Field as FieldHelper;
26
use flipbox\meta\migrations\ContentTable;
27
use flipbox\meta\records\Meta as MetaRecord;
28
use flipbox\meta\web\assets\input\Input;
29
use flipbox\meta\web\assets\settings\Settings as MetaSettingsAsset;
30
use yii\base\Component;
31
use yii\base\Exception;
32
33
/**
34
 * @author Flipbox Factory <[email protected]>
35
 * @since 1.0.0
36
 */
37
class Configuration extends Component
38
{
39
    /**
40
     * @param MetaField $metaField
41
     * @return bool
42
     * @throws \Exception
43
     * @throws \Throwable
44
     * @throws \yii\db\Exception
45
     */
46
    public function save(MetaField $metaField)
47
    {
48
        $transaction = Craft::$app->getDb()->beginTransaction();
49
        try {
50
            $contentService = Craft::$app->getContent();
51
52
            // Create/Rename table
53
            $this->ensureTable($metaField);
54
55
            // Get the originals
56
            $originalContentTable = $contentService->contentTable;
57
            $originalFieldContext = $contentService->fieldContext;
58
            $originalFieldColumnPrefix = $contentService->fieldColumnPrefix;
59
60
            // Set our content table
61
            $contentService->contentTable = FieldHelper::getContentTableName($metaField->id);
62
            $contentService->fieldContext = FieldHelper::getContextById($metaField->id);
63
            $contentService->fieldColumnPrefix = 'field_';
64
65
            // Delete old fields
66
            $this->deleteOldFields($metaField);
67
68
            // Save fields
69
            $this->saveNewFields($metaField);
70
71
            // Revert to originals
72
            $contentService->contentTable = $originalContentTable;
73
            $contentService->fieldContext = $originalFieldContext;
74
            $contentService->fieldColumnPrefix = $originalFieldColumnPrefix;
75
76
            // Save the fieldLayoutId via settings
77
            /** @var FieldRecord $fieldRecord */
78
            $fieldRecord = FieldRecord::findOne($metaField->id);
79
            $fieldRecord->settings = $metaField->getSettings();
80
81
            if (!$fieldRecord->save(true, ['settings'])) {
82
                $metaField->addError(
83
                    'settings',
84
                    Craft::t('meta', 'Unable to save settings.')
85
                );
86
                $transaction->rollback();
87
                return false;
88
            }
89
        } catch (\Exception $e) {
90
            $transaction->rollback();
91
            throw $e;
92
        }
93
94
        $transaction->commit();
95
        return true;
96
    }
97
98
    /**
99
     * @param MetaField $field
100
     * @return bool
101
     * @throws \Exception
102
     * @throws \yii\db\Exception
103
     */
104
    public function beforeDelete(MetaField $field)
105
    {
106
        $transaction = Craft::$app->getDb()->beginTransaction();
107
        try {
108
109
            // First delete the elements
110
            $elements = Meta::find()
111
                ->fieldId($field->id)
112
                ->all();
113
114
            foreach ($elements as $element) {
115
                Craft::$app->getElements()->deleteElement($element);
116
            }
117
118
            /** @var FieldLayout $fieldLayout */
119
            $fieldLayout = $field->getFieldLayout();
120
121
            // Get content table name
122
            $contentTableName = FieldHelper::getContentTableName($field->id);
123
124
            $contentService = Craft::$app->getContent();
125
126
            // Get the originals
127
            $originalContentTable = $contentService->contentTable;
128
            $originalFieldContext = $contentService->fieldContext;
129
            $originalFieldColumnPrefix = $contentService->fieldColumnPrefix;
130
131
            // Set our content table
132
            $contentService->contentTable = $contentTableName;
133
            $contentService->fieldContext = FieldHelper::getContextById($field->id);
134
            $contentService->fieldColumnPrefix = 'field_';
135
136
            // Delete fields
137
            foreach ($fieldLayout->getFields() as $field) {
138
                Craft::$app->getFields()->deleteField($field);
139
            }
140
141
            // Revert to originals
142
            $contentService->contentTable = $originalContentTable;
143
            $contentService->fieldContext = $originalFieldContext;
144
            $contentService->fieldColumnPrefix = $originalFieldColumnPrefix;
145
146
            // Drop the content table
147
            Craft::$app->getDb()->createCommand()
148
                ->dropTableIfExists($contentTableName)
149
                ->execute();
150
151
            // Delete field layout
152
            Craft::$app->getFields()->deleteLayout($fieldLayout);
153
154
            $transaction->commit();
155
156
            return true;
157
        } catch (\Exception $e) {
158
            // Revert
159
            $transaction->rollback();
160
161
            throw $e;
162
        }
163
    }
164
165
166
    /**
167
     * @param MetaField $metaField
168
     * @return bool
169
     */
170
    public function validate(MetaField $metaField): bool
171
    {
172
        $validates = true;
173
174
        // Can't validate multiple new rows at once so we'll need to give these temporary context to avoid false unique
175
        // handle validation errors, and just validate those manually. Also apply the future fieldColumnPrefix so that
176
        // field handle validation takes its length into account.
177
        $contentService = Craft::$app->getContent();
178
        $originalFieldContext = $contentService->fieldContext;
179
180
        $contentService->fieldContext = StringHelper::randomString(10);
181
182
        /** @var FieldRecord $field */
183
        foreach ($metaField->getFieldLayout()->getFields() as $field) {
184
            // Hack to allow blank field names
185
            if (!$field->name) {
186
                $field->name = '__blank__';
187
            }
188
189
            if (!$field->validate()) {
190
                $metaField->hasFieldErrors = true;
191
                $validates = false;
192
            }
193
        }
194
195
        $contentService->fieldContext = $originalFieldContext;
196
197
        return $validates;
198
    }
199
200
    /**
201
     * @inheritdoc
202
     */
203
    private function createContentTable($tableName)
204
    {
205
        $migration = new ContentTable([
206
            'tableName' => $tableName
207
        ]);
208
209
        ob_start();
210
        $migration->up();
211
        ob_end_clean();
212
    }
213
214
215
    /*******************************************
216
     * HTML
217
     *******************************************/
218
219
    /**
220
     * @param MetaField $field
221
     * @param $value
222
     * @param ElementInterface|null $element
223
     * @return string
224
     * @throws Exception
225
     * @throws \Twig_Error_Loader
226
     * @throws \yii\base\InvalidConfigException
227
     */
228
    public function getInputHtml(MetaField $field, $value, ElementInterface $element = null): string
229
    {
230
        $id = Craft::$app->getView()->formatInputId($field->handle);
231
232
        // Get the field data
233
        $fieldInfo = $this->getFieldInfoForInput($field, $element);
234
235
        Craft::$app->getView()->registerAssetBundle(Input::class);
236
237
        Craft::$app->getView()->registerJs(
238
            'new Craft.MetaInput(' .
239
            '"' . Craft::$app->getView()->namespaceInputId($id) . '", ' .
240
            Json::encode($fieldInfo, JSON_UNESCAPED_UNICODE) . ', ' .
241
            '"' . Craft::$app->getView()->namespaceInputName($field->handle) . '", ' .
242
            ($field->min ?: 'null') . ', ' .
243
            ($field->max ?: 'null') .
244
            ');'
245
        );
246
247
        Craft::$app->getView()->registerTranslations('meta', [
248
            'Add new',
249
            'Add new above'
250
        ]);
251
252
        if ($value instanceof MetaQuery) {
253
            $value
254
                ->limit(null)
255
                ->status(null)
256
                ->enabledForSite(false);
257
        }
258
259
        return Craft::$app->getView()->renderTemplate(
260
            FieldHelper::TEMPLATE_PATH . '/input',
261
            [
262
                'id' => $id,
263
                'name' => $field->handle,
264
                'field' => $field,
265
                'elements' => $value->all(),
266
                'static' => false,
267
                'template' => $field::DEFAULT_TEMPLATE
268
            ]
269
        );
270
    }
271
272
    /**
273
     * @inheritdoc
274
     */
275
    public function getSettingsHtml(MetaField $field)
276
    {
277
        // Get the available field types data
278
        $fieldTypeInfo = $this->getFieldOptionsForConfiguration();
279
280
        $view = Craft::$app->getView();
281
282
        $view->registerAssetBundle(MetaSettingsAsset::class);
283
        $view->registerJs(
284
            'new Craft.MetaConfiguration(' .
285
            Json::encode($fieldTypeInfo, JSON_UNESCAPED_UNICODE) . ', ' .
286
            Json::encode(Craft::$app->getView()->getNamespace(), JSON_UNESCAPED_UNICODE) .
287
            ');'
288
        );
289
290
        $view->registerTranslations('meta', [
291
            'New field'
292
        ]);
293
294
        $fieldTypeOptions = [];
295
296
        /** @var Field|string $class */
297
        foreach (Craft::$app->getFields()->getAllFieldTypes() as $class) {
298
            $fieldTypeOptions[] = [
299
                'value' => $class,
300
                'label' => $class::displayName()
301
            ];
302
        }
303
304
//        // Handle missing fields
305
//        $fields = $field->getFields();
306
//        foreach ($fields as $i => $field) {
307
//            if ($field instanceof MissingField) {
308
//                $fields[$i] = $field->createFallback(PlainText::class);
309
//                $fields[$i]->addError('type', Craft::t('app', 'The field type “{type}” could not be found.', [
310
//                    'type' => $field->expectedType
311
//                ]));
312
//                $field->hasFieldErrors = true;
313
//            }
314
//        }
315
//        $field->setFields($fields);
316
317
        return Craft::$app->getView()->renderTemplate(
318
            FieldHelper::TEMPLATE_PATH . '/settings',
319
            [
320
                'field' => $field,
321
                'fieldTypes' => $fieldTypeOptions,
322
                'defaultTemplate' => $field::DEFAULT_TEMPLATE
323
            ]
324
        );
325
    }
326
327
    /**
328
     * TODO - eliminate this and render configuration via ajax call
329
     *
330
     * Returns html for all associated field types for the Meta field input.
331
     *
332
     * @return array
333
     */
334
    private function getFieldInfoForInput(MetaField $field, ElementInterface $element = null): array
335
    {
336
        // Set a temporary namespace for these
337
        $originalNamespace = Craft::$app->getView()->getNamespace();
338
        $namespace = Craft::$app->getView()->namespaceInputName(
339
            $field->handle . '[__META__][fields]',
340
            $originalNamespace
341
        );
342
        Craft::$app->getView()->setNamespace($namespace);
343
344
        // Create a fake meta so the field types have a way to get at the owner element, if there is one
345
        $meta = new Meta();
346
        $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...
347
348
        if ($element) {
349
            $meta->setOwner($element);
350
            $meta->siteId = $element->siteId;
351
        }
352
353
        $fieldLayoutFields = $field->getFieldLayout()->getFields();
354
355
        // Set $_isFresh's
356
        foreach ($fieldLayoutFields as $field) {
357
            $field->setIsFresh(true);
358
        }
359
360
        Craft::$app->getView()->startJsBuffer();
361
362
        $bodyHtml = Craft::$app->getView()->namespaceInputs(
363
            Craft::$app->getView()->renderTemplate(
364
                '_includes/fields',
365
                [
366
                    'namespace' => null,
367
                    'fields' => $fieldLayoutFields,
368
                    'element' => $meta
369
                ]
370
            )
371
        );
372
373
        // Reset $_isFresh's
374
        foreach ($fieldLayoutFields as $field) {
375
            $field->setIsFresh(null);
376
        }
377
378
        $footHtml = Craft::$app->getView()->clearJsBuffer();
379
380
        $fields = [
381
            'bodyHtml' => $bodyHtml,
382
            'footHtml' => $footHtml,
383
        ];
384
385
        // Revert namespace
386
        Craft::$app->getView()->setNamespace($originalNamespace);
387
388
        return $fields;
389
    }
390
391
    /**
392
     *
393
     * TODO - eliminate this and render configuration via ajax call
394
     *
395
     * Returns info about each field type for the configurator.
396
     *
397
     * @return array
398
     */
399
    private function getFieldOptionsForConfiguration()
400
    {
401
        $disallowedFields = [
402
            MetaField::class,
403
            Matrix::class
404
        ];
405
406
        if (Craft::$app->getPlugins()->getPlugin('super-table')) {
407
            $disallowedFields[] = \verbb\supertable\fields\SuperTableField::class;
408
        }
409
410
        $fieldTypes = [];
411
412
        // Set a temporary namespace for these
413
        $originalNamespace = Craft::$app->getView()->getNamespace();
414
        $namespace = Craft::$app->getView()->namespaceInputName('fields[__META_FIELD__][settings]', $originalNamespace);
415
        Craft::$app->getView()->setNamespace($namespace);
416
417
        /** @var Field|string $class */
418
        foreach (Craft::$app->getFields()->getAllFieldTypes() as $class) {
419
            // Ignore disallowed fields
420
            if (in_array($class, $disallowedFields)) {
421
                continue;
422
            }
423
424
            Craft::$app->getView()->startJsBuffer();
425
426
            /** @var FieldInterface $field */
427
            $field = new $class();
428
429
            if ($settingsHtml = (string)$field->getSettingsHtml()) {
430
                $settingsHtml = Craft::$app->getView()->namespaceInputs($settingsHtml);
431
            }
432
433
            $settingsBodyHtml = $settingsHtml;
434
            $settingsFootHtml = Craft::$app->getView()->clearJsBuffer();
435
436
            $fieldTypes[] = [
437
                'type' => $class,
438
                'name' => $class::displayName(),
439
                'settingsBodyHtml' => $settingsBodyHtml,
440
                'settingsFootHtml' => $settingsFootHtml,
441
            ];
442
        }
443
444
        Craft::$app->getView()->setNamespace($originalNamespace);
445
446
        return $fieldTypes;
447
    }
448
449
    /**
450
     * @param MetaField $metaField
451
     * @throws Exception
452
     * @throws \Throwable
453
     */
454
    private function deleteOldFields(MetaField $metaField)
455
    {
456
        /** @var \craft\services\Fields $fieldsService */
457
        $fieldsService = Craft::$app->getFields();
458
459
        // Get existing fields
460
        $oldFields = $fieldsService->getAllFields(FieldHelper::getContextById($metaField->id));
461
        $oldFieldsById = ArrayHelper::index($oldFields, 'id');
462
463
        /** @var \craft\base\Field $field */
464
        foreach ($metaField->getFieldLayout()->getFields() as $field) {
465
            if (!$field->getIsNew()) {
466
                ArrayHelper::remove($oldFieldsById, $field->id);
467
            }
468
        }
469
470
        // Drop the old fields
471
        foreach ($oldFieldsById as $field) {
472
            if (!$fieldsService->deleteField($field)) {
473
                throw new Exception(Craft::t('app', 'An error occurred while deleting this Meta field.'));
474
            }
475
        }
476
477
        // Refresh the schema cache
478
        Craft::$app->getDb()->getSchema()->refresh();
479
    }
480
481
    /**
482
     * @param MetaField $metaField
483
     * @throws Exception
484
     * @throws \Throwable
485
     */
486
    private function saveNewFields(MetaField $metaField)
487
    {
488
        $fieldLayoutFields = [];
489
        $sortOrder = 0;
490
491
        /** @var \craft\services\Fields $fieldsService */
492
        $fieldsService = Craft::$app->getFields();
493
494
        // Save field
495
        /** @var \craft\base\Field $field */
496
        foreach ($metaField->getFieldLayout()->getFields() as $field) {
497
            // Save field (we validated earlier)
498
            if (!$fieldsService->saveField($field, false)) {
499
                throw new Exception('An error occurred while saving this Meta field.');
500
            }
501
502
            // Set sort order
503
            $field->sortOrder = ++$sortOrder;
504
505
            $fieldLayoutFields[] = $field;
506
        }
507
508
        $fieldLayout = $metaField->getFieldLayout();
509
510
        $fieldLayoutTab = new FieldLayoutTab([
511
            'name' => 'Fields',
512
            'sortOrder' => 1,
513
            'fields' => $fieldLayoutFields
514
        ]);
515
516
        $fieldLayout->setTabs([$fieldLayoutTab]);
517
        $fieldLayout->setFields($fieldLayoutFields);
518
519
        $fieldsService->saveLayout($fieldLayout);
520
521
        // Update the element & record with our new field layout ID
522
        $metaField->setFieldLayout($fieldLayout);
523
        $metaField->fieldLayoutId = (int)$fieldLayout->id;
524
    }
525
526
    /**
527
     * @param MetaField $metaField
528
     */
529
    private function ensureTable(MetaField $metaField)
530
    {
531
        // Create the content table first since the element fields will need it
532
        $contentTable = FieldHelper::getContentTableName($metaField->id);
533
534
        // Do we need to create/rename the content table?
535
        if (!Craft::$app->getDb()->tableExists($contentTable)) {
536
            $this->createContentTable($contentTable);
537
            Craft::$app->getDb()->getSchema()->refresh();
538
        }
539
    }
540
}
541