Completed
Push — develop ( e47b53...a63fce )
by Nate
08:38
created

Configuration::getFieldInfoForInput()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 46
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 46
ccs 0
cts 33
cp 0
rs 8.9411
c 0
b 0
f 0
cc 3
eloc 23
nc 4
nop 1
crap 12
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\fields\Meta as MetaField;
23
use flipbox\meta\helpers\Field as FieldHelper;
24
use flipbox\meta\migrations\ContentTable;
25
use flipbox\meta\records\Meta as MetaRecord;
26
use flipbox\meta\web\assets\input\Input;
27
use flipbox\meta\web\assets\settings\Settings as MetaSettingsAsset;
28
use yii\base\Component;
29
use yii\base\Exception;
30
31
/**
32
 * @author Flipbox Factory <[email protected]>
33
 * @since 1.0.0
34
 */
35
class Configuration extends Component
36
{
37
    /**
38
     * @param MetaField $metaField
39
     * @return bool
40
     * @throws \Exception
41
     * @throws \Throwable
42
     * @throws \yii\db\Exception
43
     */
44
    public function afterSave(MetaField $metaField)
45
    {
46
47
        $transaction = Craft::$app->getDb()->beginTransaction();
48
        try {
49
50
            /** @var \craft\services\Content $contentService */
51
            $contentService = Craft::$app->getContent();
52
53
            /** @var \craft\services\Fields $fieldsService */
54
            $fieldsService = Craft::$app->getFields();
55
56
            // Create the content table first since the element fields will need it
57
            $contentTable = FieldHelper::getContentTableName($metaField->id);
58
59
            // Get the originals
60
            $originalContentTable = $contentService->contentTable;
61
            $originalFieldContext = $contentService->fieldContext;
62
            $originalFieldPrefix = $contentService->fieldColumnPrefix;
63
            $originalOldFieldPrefix = $fieldsService->oldFieldColumnPrefix;
64
65
            // Set our content table
66
            $contentService->contentTable = $contentTable;
67
            $contentService->fieldColumnPrefix = 'field_';
68
            $fieldsService->oldFieldColumnPrefix = 'field_';
69
70
            // Set our field context
71
            $contentService->fieldContext = FieldHelper::getContextById($metaField->id);
72
73
            // Get existing fields
74
            $oldFieldsById = ArrayHelper::index($fieldsService->getAllFields(), 'id');
75
76
            /** @var \craft\base\Field $field */
77
            foreach ($metaField->getFieldLayout()->getFields() as $field) {
78
                if (!$field->getIsNew()) {
79
                    ArrayHelper::remove($oldFieldsById, $field->id);
1 ignored issue
show
Bug introduced by
Accessing id on the interface craft\base\FieldInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
80
                }
81
            }
82
83
            // Drop the old fields
84
            foreach ($oldFieldsById as $field) {
85
                if (!$fieldsService->deleteField($field)) {
86
                    throw new Exception(Craft::t('app', 'An error occurred while deleting this Meta field.'));
87
                }
88
            }
89
90
            // Refresh the schema cache
91
            Craft::$app->getDb()->getSchema()->refresh();
92
93
            // Do we need to create/rename the content table?
94
            if (!Craft::$app->getDb()->tableExists($contentTable)) {
95
                $this->createContentTable($contentTable);
96
                Craft::$app->getDb()->getSchema()->refresh();
97
            }
98
99
            // Save the fields and field layout
100
            // -------------------------------------------------------------
101
102
            $fieldLayoutFields = [];
103
            $sortOrder = 0;
104
105
            // Set our content table
106
            $contentService->contentTable = $contentTable;
107
108
            // Save field
109
            /** @var \craft\base\Field $field */
110
            foreach ($metaField->getFieldLayout()->getFields() as $field) {
111
                // Save field (we validated earlier)
112
                if (!$fieldsService->saveField($field, false)) {
113
                    throw new Exception('An error occurred while saving this Meta field.');
114
                }
115
116
                // Set sort order
117
                $field->sortOrder = ++$sortOrder;
1 ignored issue
show
Bug introduced by
Accessing sortOrder on the interface craft\base\FieldInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
118
119
                $fieldLayoutFields[] = $field;
120
            }
121
122
            // Revert to originals
123
            $contentService->contentTable = $originalContentTable;
124
            $contentService->fieldContext = $originalFieldContext;
125
            $contentService->fieldColumnPrefix = $originalFieldPrefix;
126
            $fieldsService->oldFieldColumnPrefix = $originalOldFieldPrefix;
127
128
            $fieldLayout = $metaField->getFieldLayout();
129
130
            $fieldLayoutTab = new FieldLayoutTab();
131
            $fieldLayoutTab->name = 'Fields';
132
            $fieldLayoutTab->sortOrder = 1;
133
            $fieldLayoutTab->setFields($fieldLayoutFields);
134
135
            $fieldLayout->setTabs([$fieldLayoutTab]);
136
            $fieldLayout->setFields($fieldLayoutFields);
137
138
            $fieldsService->saveLayout($fieldLayout);
139
140
            // Update the element & record with our new field layout ID
141
            $metaField->setFieldLayout($fieldLayout);
142
            $metaField->fieldLayoutId = (int)$fieldLayout->id;
143
144
            // Save the fieldLayoutId via settings
145
            /** @var FieldRecord $fieldRecord */
146
            $fieldRecord = FieldRecord::findOne($metaField->id);
147
            $fieldRecord->settings = $metaField->getSettings();
148
149
            if ($fieldRecord->save(true, ['settings'])) {
150
                // Commit field changes
151
                $transaction->commit();
152
153
                return true;
154
            } else {
155
                $metaField->addError('settings', Craft::t('meta', 'Unable to save settings.'));
156
            }
157
        } catch (\Exception $e) {
158
            $transaction->rollback();
159
160
            throw $e;
161
        }
162
163
        $transaction->rollback();
164
165
        return false;
166
    }
167
168
    /**
169
     * @param MetaField $field
170
     * @return bool
171
     * @throws \Exception
172
     * @throws \yii\db\Exception
173
     */
174
    public function beforeDelete(MetaField $field)
175
    {
176
        $transaction = Craft::$app->getDb()->beginTransaction();
177
        try {
178
            // Delete field layout
179
            Craft::$app->getFields()->deleteLayoutById($field->fieldLayoutId);
180
181
            // Get content table name
182
            $contentTableName = FieldHelper::getContentTableName($field->id);
183
184
            // Drop the content table
185
            Craft::$app->getDb()->createCommand()
186
                ->dropTableIfExists($contentTableName)
187
                ->execute();
188
189
            // find any of the context fields
190
            $subFieldRecords = FieldRecord::find()
191
                ->andWhere(['like', 'context', MetaRecord::tableAlias() . ':%', false])
192
                ->all();
193
194
            // Delete them
195
            /** @var MetaRecord $subFieldRecord */
196
            foreach ($subFieldRecords as $subFieldRecord) {
197
                Craft::$app->getFields()->deleteFieldById($subFieldRecord->id);
198
            }
199
200
            $transaction->commit();
201
            return true;
202
        } catch (\Exception $e) {
203
            // Revert
204
            $transaction->rollback();
205
206
            throw $e;
207
        }
208
    }
209
210
211
    /**
212
     * @param MetaField $metaField
213
     * @return bool
214
     */
215
    public function validate(MetaField $metaField): bool
216
    {
217
        $validates = true;
218
219
        // Can't validate multiple new rows at once so we'll need to give these temporary context to avoid false unique
220
        // handle validation errors, and just validate those manually. Also apply the future fieldColumnPrefix so that
221
        // field handle validation takes its length into account.
222
        $contentService = Craft::$app->getContent();
223
        $originalFieldContext = $contentService->fieldContext;
224
225
        $contentService->fieldContext = StringHelper::randomString(10);
226
227
        /** @var FieldRecord $field */
228
        foreach ($metaField->getFieldLayout()->getFields() as $field) {
229
            // Hack to allow blank field names
230
            if (!$field->name) {
1 ignored issue
show
Bug introduced by
Accessing name on the interface craft\base\FieldInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
231
                $field->name = '__blank__';
1 ignored issue
show
Bug introduced by
Accessing name on the interface craft\base\FieldInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
232
            }
233
234
            if (!$field->validate()) {
235
                $metaField->hasFieldErrors = true;
236
                $validates = false;
237
            }
238
        }
239
240
        $contentService->fieldContext = $originalFieldContext;
241
242
        return $validates;
243
    }
244
245
    /**
246
     * @inheritdoc
247
     */
248
    private function createContentTable($tableName)
249
    {
250
        $migration = new ContentTable([
251
            'tableName' => $tableName
252
        ]);
253
254
        ob_start();
255
        $migration->up();
256
        ob_end_clean();
257
    }
258
259
260
    /*******************************************
261
     * HTML
262
     *******************************************/
263
264
    /**
265
     * @param MetaField $field
266
     * @param $value
267
     * @param ElementInterface|null $element
268
     * @return string
269
     * @throws Exception
270
     * @throws \Twig_Error_Loader
271
     * @throws \yii\base\InvalidConfigException
272
     */
273
    public function getInputHtml(MetaField $field, $value, ElementInterface $element = null): string
0 ignored issues
show
Unused Code introduced by
The parameter $element is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
274
    {
275
        $id = Craft::$app->getView()->formatInputId($field->handle);
276
277
        // Get the field data
278
        $fieldInfo = $this->getFieldInfoForInput($field);
279
280
        Craft::$app->getView()->registerAssetBundle(Input::class);
281
282
        Craft::$app->getView()->registerJs(
283
            'new Craft.MetaInput(' .
284
            '"' . Craft::$app->getView()->namespaceInputId($id) . '", ' .
285
            Json::encode($fieldInfo, JSON_UNESCAPED_UNICODE) . ', ' .
286
            '"' . Craft::$app->getView()->namespaceInputName($field->handle) . '", ' .
287
            ($field->min ?: 'null') . ', ' .
288
            ($field->max ?: 'null') .
289
            ');'
290
        );
291
292
        Craft::$app->getView()->registerTranslations('meta', [
293
            'Add new',
294
            'Add new above'
295
        ]);
296
297
        if ($value instanceof MetaQuery) {
298
            $value
299
                ->limit(null)
300
                ->status(null)
301
                ->enabledForSite(false);
302
        }
303
304
        return Craft::$app->getView()->renderTemplate(
305
            FieldHelper::TEMPLATE_PATH . DIRECTORY_SEPARATOR . 'input',
306
            [
307
                'id' => $id,
308
                'name' => $field->handle,
309
                'field' => $field,
310
                'elements' => $value,
311
                'static' => false,
312
                'template' => $field::DEFAULT_TEMPLATE
313
            ]
314
        );
315
    }
316
317
    /**
318
     * @inheritdoc
319
     */
320
    public function getSettingsHtml(MetaField $field)
321
    {
322
        // Get the available field types data
323
        $fieldTypeInfo = $this->getFieldOptionsForConfiguration();
324
325
        $view = Craft::$app->getView();
326
327
        $view->registerAssetBundle(MetaSettingsAsset::class);
328
        $view->registerJs(
329
            'new Craft.MetaConfiguration(' .
330
            Json::encode($fieldTypeInfo, JSON_UNESCAPED_UNICODE) . ', ' .
331
            Json::encode(Craft::$app->getView()->getNamespace(), JSON_UNESCAPED_UNICODE) .
332
            ');'
333
        );
334
335
        $view->registerTranslations('meta', [
336
            'New field'
337
        ]);
338
339
        $fieldTypeOptions = [];
340
341
        /** @var Field|string $class */
342
        foreach (Craft::$app->getFields()->getAllFieldTypes() as $class) {
343
            $fieldTypeOptions[] = [
344
                'value' => $class,
345
                'label' => $class::displayName()
346
            ];
347
        }
348
349
//        // Handle missing fields
350
//        $fields = $field->getFields();
351
//        foreach ($fields as $i => $field) {
352
//            if ($field instanceof MissingField) {
353
//                $fields[$i] = $field->createFallback(PlainText::class);
354
//                $fields[$i]->addError('type', Craft::t('app', 'The field type “{type}” could not be found.', [
355
//                    'type' => $field->expectedType
356
//                ]));
357
//                $field->hasFieldErrors = true;
358
//            }
359
//        }
360
//        $field->setFields($fields);
361
362
        return Craft::$app->getView()->renderTemplate(
363
            FieldHelper::TEMPLATE_PATH . DIRECTORY_SEPARATOR . 'settings',
364
            [
365
                'field' => $field,
366
                'fieldTypes' => $fieldTypeOptions,
367
                'defaultTemplate' => $field::DEFAULT_TEMPLATE
368
            ]
369
        );
370
    }
371
372
    /**
373
     * TODO - eliminate this and render configuration via ajax call
374
     *
375
     * Returns html for all associated field types for the Meta field input.
376
     *
377
     * @return array
378
     */
379
    private function getFieldInfoForInput(MetaField $field): array
380
    {
381
        // Set a temporary namespace for these
382
        $originalNamespace = Craft::$app->getView()->getNamespace();
383
        $namespace = Craft::$app->getView()->namespaceInputName(
384
            $field->handle . '[__META__][fields]',
385
            $originalNamespace
386
        );
387
        Craft::$app->getView()->setNamespace($namespace);
388
389
        $fieldLayoutFields = $field->getFieldLayout()->getFields();
390
391
        // Set $_isFresh's
392
        foreach ($fieldLayoutFields as $field) {
393
            $field->setIsFresh(true);
394
        }
395
396
        Craft::$app->getView()->startJsBuffer();
397
398
        $bodyHtml = Craft::$app->getView()->namespaceInputs(
399
            Craft::$app->getView()->renderTemplate(
400
                '_includes/fields',
401
                [
402
                    'namespace' => null,
403
                    'fields' => $fieldLayoutFields
404
                ]
405
            )
406
        );
407
408
        // Reset $_isFresh's
409
        foreach ($fieldLayoutFields as $field) {
410
            $field->setIsFresh(null);
411
        }
412
413
        $footHtml = Craft::$app->getView()->clearJsBuffer();
414
415
        $fields = [
416
            'bodyHtml' => $bodyHtml,
417
            'footHtml' => $footHtml,
418
        ];
419
420
        // Revert namespace
421
        Craft::$app->getView()->setNamespace($originalNamespace);
422
423
        return $fields;
424
    }
425
426
    /**
427
     *
428
     * TODO - eliminate this and render configuration via ajax call
429
     *
430
     * Returns info about each field type for the configurator.
431
     *
432
     * @return array
433
     */
434
    private function getFieldOptionsForConfiguration()
435
    {
436
        $disallowedFields = [
437
            MetaField::class,
438
            Matrix::class
439
        ];
440
441
        $fieldTypes = [];
442
443
        // Set a temporary namespace for these
444
        $originalNamespace = Craft::$app->getView()->getNamespace();
445
        $namespace = Craft::$app->getView()->namespaceInputName('fields[__META_FIELD__][settings]', $originalNamespace);
446
        Craft::$app->getView()->setNamespace($namespace);
447
448
        /** @var Field|string $class */
449
        foreach (Craft::$app->getFields()->getAllFieldTypes() as $class) {
450
            // Ignore disallowed fields
451
            if (in_array($class, $disallowedFields)) {
452
                continue;
453
            }
454
455
            Craft::$app->getView()->startJsBuffer();
456
457
            /** @var FieldInterface $field */
458
            $field = new $class();
459
460
            if ($settingsHtml = (string)$field->getSettingsHtml()) {
461
                $settingsHtml = Craft::$app->getView()->namespaceInputs($settingsHtml);
462
            }
463
464
            $settingsBodyHtml = $settingsHtml;
465
            $settingsFootHtml = Craft::$app->getView()->clearJsBuffer();
466
467
            $fieldTypes[] = [
468
                'type' => $class,
469
                'name' => $class::displayName(),
470
                'settingsBodyHtml' => $settingsBodyHtml,
471
                'settingsFootHtml' => $settingsFootHtml,
472
            ];
473
        }
474
475
        Craft::$app->getView()->setNamespace($originalNamespace);
476
477
        return $fieldTypes;
478
    }
479
}
480