Meta::supportedTranslationMethods()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 0
cts 6
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 0
crap 2
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\fields;
10
11
use Craft;
12
use craft\base\EagerLoadingFieldInterface;
13
use craft\base\Element;
14
use craft\base\ElementInterface;
15
use craft\base\Field;
16
use craft\base\FieldInterface;
17
use craft\behaviors\FieldLayoutBehavior;
18
use craft\db\Query;
19
use craft\elements\db\ElementQuery;
20
use craft\elements\db\ElementQueryInterface;
21
use craft\fields\Matrix;
22
use craft\fields\MissingField;
23
use craft\fields\PlainText;
24
use craft\helpers\Json;
25
use craft\helpers\StringHelper;
26
use craft\models\FieldLayout;
27
use craft\validators\ArrayValidator;
28
use flipbox\meta\elements\db\Meta as MetaQuery;
29
use flipbox\meta\elements\Meta as MetaElement;
30
use flipbox\meta\helpers\Field as FieldHelper;
31
use flipbox\meta\Meta as MetaPlugin;
32
use flipbox\meta\records\Meta as MetaRecord;
33
use flipbox\meta\web\assets\input\Input as MetaInputAsset;
34
use flipbox\meta\web\assets\settings\Settings as MetaSettingsAsset;
35
36
/**
37
 * @author Flipbox Factory <[email protected]>
38
 * @since 1.0.0
39
 *
40
 * @method setFieldLayout(FieldLayout $fieldLayout)
41
 * @method FieldLayout getFieldLayout()
42
 */
43
class Meta extends Field implements EagerLoadingFieldInterface
44
{
45
46
    const DEFAULT_TEMPLATE = FieldHelper::TEMPLATE_PATH . DIRECTORY_SEPARATOR . 'layout';
47
48
    /**
49
     * @var int|null
50
     */
51
    public $max;
52
53
    /**
54
     * @var int|null
55
     */
56
    public $min;
57
58
    /**
59
     * @var string
60
     */
61
    public $selectionLabel = "Add meta";
62
63
    /**
64
     * @var int
65
     */
66
    public $localize = false;
67
68
    /**
69
     * @var int|null
70
     */
71
    public $fieldLayoutId;
72
73
    /**
74
     * @var bool
75
     */
76
    public $templateOverride = false;
77
78
    /**
79
     * @var string
80
     */
81
    protected $template = self::DEFAULT_TEMPLATE;
82
83
    /**
84
     * @var bool
85
     */
86
    public $hasFieldErrors = false;
87
88
    /**
89
     * @inheritdoc
90
     */
91
    public static function displayName(): string
92
    {
93
        return Craft::t('meta', 'Meta');
94
    }
95
96
    /**
97
     * @inheritdoc
98
     */
99
    public static function supportedTranslationMethods(): array
100
    {
101
        // Don't ever automatically propagate values to other sites.
102
        return [
103
            self::TRANSLATION_METHOD_SITE,
104
        ];
105
    }
106
107
    /**
108
     * @inheritdoc
109
     */
110
    public static function hasContentColumn(): bool
111
    {
112
        return false;
113
    }
114
115
    /**
116
     * @inheritdoc
117
     */
118
    public function settingsAttributes(): array
119
    {
120
        return [
121
            'max',
122
            'min',
123
            'selectionLabel',
124
            'fieldLayoutId',
125
            'templateOverride',
126
            'template'
127
        ];
128
    }
129
130
    /**
131
     * @inheritdoc
132
     */
133
    public function behaviors()
134
    {
135
        return [
136
            'fieldLayout' => [
137
                'class' => FieldLayoutBehavior::class,
138
                'elementType' => self::class
139
            ],
140
        ];
141
    }
142
143
    /**
144
     * @inheritdoc
145
     */
146
    public function rules()
147
    {
148
        return array_merge(
149
            parent::rules(),
150
            [
151
                [
152
                    [
153
                        'min',
154
                        'max'
155
                    ],
156
                    'integer',
157
                    'min' => 0
158
                ]
159
            ]
160
        );
161
    }
162
163
    /**
164
     * @inheritdoc
165
     */
166
    public function validate($attributeNames = null, $clearErrors = true): bool
167
    {
168
        // Run basic model validation first
169
        $validates = parent::validate($attributeNames, $clearErrors);
170
171
        // Run field validation as well
172
        if (!MetaPlugin::getInstance()->getConfiguration()->validate($this)) {
173
            $validates = false;
174
        }
175
176
        return $validates;
177
    }
178
179
    /**
180
     * @inheritdoc
181
     */
182
    public function getSettingsHtml()
183
    {
184
        // Get the available field types data
185
        $fieldTypeInfo = $this->getFieldOptionsForConfiguration();
186
187
        $view = Craft::$app->getView();
188
189
        $view->registerAssetBundle(MetaSettingsAsset::class);
190
        $view->registerJs(
191
            'new Craft.MetaConfiguration(' .
192
            Json::encode($fieldTypeInfo, JSON_UNESCAPED_UNICODE) . ', ' .
193
            Json::encode(Craft::$app->getView()->getNamespace(), JSON_UNESCAPED_UNICODE) .
194
            ');'
195
        );
196
197
        $view->registerTranslations('meta', [
198
            'New field'
199
        ]);
200
201
        $fieldTypeOptions = [];
202
203
        /** @var Field|string $class */
204
        foreach (Craft::$app->getFields()->getAllFieldTypes() as $class) {
205
            $fieldTypeOptions[] = [
206
                'value' => $class,
207
                'label' => $class::displayName()
208
            ];
209
        }
210
211
        // Handle missing fields
212
        $fields = $this->getFields();
213
        foreach ($fields as $i => $field) {
214
            if ($field instanceof MissingField) {
215
                $fields[$i] = $field->createFallback(PlainText::class);
216
                $fields[$i]->addError('type', Craft::t('app', 'The field type “{type}” could not be found.', [
217
                    'type' => $field->expectedType
218
                ]));
219
                $this->hasFieldErrors = true;
220
            }
221
        }
222
        $this->setFields($fields);
0 ignored issues
show
Documentation introduced by
$fields is of type array<integer,object<cra...se\ComponentInterface>>, but the function expects a array<integer,object<craft\base\FieldInterface>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
223
224
        return Craft::$app->getView()->renderTemplate(
225
            FieldHelper::TEMPLATE_PATH . DIRECTORY_SEPARATOR . 'settings',
226
            [
227
                'field' => $this,
228
                'fieldTypes' => $fieldTypeOptions
229
            ]
230
        );
231
    }
232
233
    /**
234
     * @inheritdoc
235
     */
236
    public function normalizeValue($value, ElementInterface $element = null)
237
    {
238
        /** @var Element|null $element */
239
240
        if ($value instanceof MetaQuery) {
241
            return $value;
242
        }
243
244
        // New element query
245
        $query = MetaElement::find();
246
247
        // Existing element?
248
        if ($element && $element->id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $element->id of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. 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...
249
            $query->ownerId($element->id);
250
        } else {
251
            $query->id(false);
252
        }
253
254
        // Set our field and site to the query
255
        $query
256
            ->fieldId($this->id)
257
            ->siteId($element->siteId);
258
259
        // Set the initially matched elements if $value is already set, which is the case if there was a validation
260
        // error or we're loading an entry revision.
261
        if (is_array($value) || $value === '') {
262
            $query->status = null;
263
            $query->enabledForSite = false;
264
            $query->limit = null;
265
            $query->setCachedResult(
266
                $this->createElementsFromSerializedData($value, $element)
267
            );
268
        }
269
270
        return $query;
271
    }
272
273
    /**
274
     * @inheritdoc
275
     */
276
    public function modifyElementsQuery(ElementQueryInterface $query, $value)
277
    {
278
        /** @var ElementQuery $query */
279
        if ($value === 'not :empty:') {
280
            $value = ':notempty:';
281
        }
282
283
        if ($value === ':notempty:' || $value === ':empty:') {
284
            $alias = MetaRecord::tableAlias() . '_' . $this->handle;
285
            $operator = ($value === ':notempty:' ? '!=' : '=');
286
287
            $query->subQuery->andWhere(
288
                "(select count([[{$alias}.id]]) from " .
289
                MetaRecord::tableName() .
290
                " {{{$alias}}} where [[{$alias}.ownerId]] = [[elements.id]]" .
291
                " and [[{$alias}.fieldId]] = :fieldId) {$operator} 0",
292
                [':fieldId' => $this->id]
293
            );
294
        } elseif ($value !== null) {
295
            return false;
296
        }
297
298
        return null;
299
    }
300
301
    /**
302
     * @return string
303
     */
304
    public function getDefaultTemplate(): string
305
    {
306
        return $this->templateOverride ? $this->template : self::DEFAULT_TEMPLATE;
307
    }
308
309
    /**
310
     * @return string
311
     */
312
    public function getTemplate(): string
313
    {
314
        return $this->templateOverride ? $this->template : self::DEFAULT_TEMPLATE;
315
    }
316
317
    /**
318
     * @param $template
319
     * @return $this
320
     */
321
    public function setTemplate($template)
322
    {
323
        if (!$this->templateOverride) {
324
            $template = null;
325
        }
326
        $this->template = $template;
327
        return $this;
328
    }
329
330
    /**
331
     * @inheritdoc
332
     */
333
    public function getInputHtml($value, ElementInterface $element = null): string
334
    {
335
        $id = Craft::$app->getView()->formatInputId($this->handle);
336
337
        // Get the field data
338
        $fieldInfo = $this->getFieldInfoForInput();
339
340
        Craft::$app->getView()->registerAssetBundle(MetaInputAsset::class);
341
342
        Craft::$app->getView()->registerJs(
343
            'new Craft.MetaInput(' .
344
            '"' . Craft::$app->getView()->namespaceInputId($id) . '", ' .
345
            Json::encode($fieldInfo, JSON_UNESCAPED_UNICODE) . ', ' .
346
            '"' . Craft::$app->getView()->namespaceInputName($this->handle) . '", ' .
347
            ($this->min ?: 'null') . ', ' .
348
            ($this->max ?: 'null') .
349
            ');'
350
        );
351
352
        Craft::$app->getView()->registerTranslations('meta', [
353
            'Add new',
354
            'Add new above'
355
        ]);
356
357
        if ($value instanceof MetaQuery) {
358
            $value
359
                ->limit(null)
360
                ->status(null)
361
                ->enabledForSite(false);
362
        }
363
364
        return Craft::$app->getView()->renderTemplate(
365
            FieldHelper::TEMPLATE_PATH . DIRECTORY_SEPARATOR . 'input',
366
            [
367
                'id' => $id,
368
                'name' => $this->handle,
369
                'field' => $this,
370
                'elements' => $value,
371
                'static' => false,
372
                'template' => self::DEFAULT_TEMPLATE
373
            ]
374
        );
375
    }
376
377
    /**
378
     * @inheritdoc
379
     */
380
    public function getElementValidationRules(): array
381
    {
382
        return [
383
            'validateMeta',
384
            [
385
                ArrayValidator::class,
386
                'max' => $this->max ?: null,
387
                'tooMany' => Craft::t(
388
                    'app',
389
                    '{attribute} should contain at most {max, number} {max, plural, one{record} other{records}}.'
390
                ),
391
            ],
392
        ];
393
    }
394
395
    /**
396
     * Validates an owner element’s Meta.
397
     *
398
     * @param ElementInterface $element
399
     *
400
     * @return void
401
     */
402
    public function validateMeta(ElementInterface $element)
403
    {
404
        /** @var Element $element */
405
        /** @var MetaQuery $value */
406
        $value = $element->getFieldValue($this->handle);
407
        $validate = true;
408
409
        foreach ($value->all() as $meta) {
410
            /** @var MetaElement $meta */
411
            if (!$meta->validate()) {
412
                $validate = false;
413
            }
414
        }
415
416
        if (!$validate) {
417
            $element->addError($this->handle, Craft::t('app', 'Correct the errors listed above.'));
418
        }
419
    }
420
421
    /**
422
     * @param mixed $value
423
     * @param Element|ElementInterface $element
424
     * @return string
425
     */
426
    public function getSearchKeywords($value, ElementInterface $element): string
427
    {
428
        /** @var MetaQuery $value */
429
430
        $keywords = [];
431
        $contentService = Craft::$app->getContent();
432
433
        /** @var MetaElement $meta */
434
        foreach ($value->all() as $meta) {
435
            $originalContentTable = $contentService->contentTable;
436
            $originalFieldContext = $contentService->fieldContext;
437
438
            $contentService->contentTable = $meta->getContentTable();
439
            $contentService->fieldContext = $meta->getFieldContext();
440
441
            /** @var Field $field */
442
            foreach (Craft::$app->getFields()->getAllFields() as $field) {
443
                $fieldValue = $meta->getFieldValue($field->handle);
444
                $keywords[] = $field->getSearchKeywords($fieldValue, $element);
445
            }
446
447
            $contentService->contentTable = $originalContentTable;
448
            $contentService->fieldContext = $originalFieldContext;
449
        }
450
451
        return parent::getSearchKeywords($keywords, $element);
452
    }
453
454
    /**
455
     * todo - review this
456
     *
457
     * @inheritdoc
458
     */
459
    public function getStaticHtml($value, ElementInterface $element): string
460
    {
461
        if ($value) {
462
            $id = StringHelper::randomString();
463
            return Craft::$app->getView()->renderTemplate(
464
                FieldHelper::TEMPLATE_PATH . DIRECTORY_SEPARATOR . 'input',
465
                [
466
                    'id' => $id,
467
                    'name' => $this->handle,
468
                    'elements' => $value,
469
                    'static' => true
470
                ]
471
            );
472
        } else {
473
            Craft::$app->getView()->registerTranslations('meta', [
474
                'No meta'
475
            ]);
476
477
            return '<p class="light">' . Craft::t('meta', 'No meta') . '</p>';
478
        }
479
    }
480
481
    /**
482
     * @inheritdoc
483
     */
484
    public function getEagerLoadingMap(array $sourceElements)
485
    {
486
        // Get the source element IDs
487
        $sourceElementIds = [];
488
489
        foreach ($sourceElements as $sourceElement) {
490
            $sourceElementIds[] = $sourceElement->id;
491
        }
492
493
        // Return any relation data on these elements, defined with this field
494
        $map = (new Query())
495
            ->select(['ownerId as source', 'id as target'])
496
            ->from([MetaRecord::tableName()])
497
            ->where([
498
                'fieldId' => $this->id,
499
                'ownerId' => $sourceElementIds,
500
            ])
501
            ->orderBy(['sortOrder' => SORT_ASC])
502
            ->all();
503
504
        return [
505
            'elementType' => MetaElement::class,
506
            'map' => $map,
507
            'criteria' => ['fieldId' => $this->id]
508
        ];
509
    }
510
511
    /**
512
     * @inheritdoc
513
     */
514
    public function beforeSave(bool $isNew): bool
515
    {
516
        // Save field settings (and field content)
517
        if (!MetaPlugin::getInstance()->getConfiguration()->beforeSave($this)) {
518
            return false;
519
        }
520
521
        // Trigger an 'afterSave' event
522
        return parent::beforeSave($isNew);
523
    }
524
525
    /**
526
     * @inheritdoc
527
     */
528
    public function afterSave(bool $isNew)
529
    {
530
        // Save field settings (and field content)
531
        MetaPlugin::getInstance()->getConfiguration()->afterSave($this);
532
533
        // Trigger an 'afterSave' event
534
        parent::afterSave($isNew);
535
    }
536
537
    /**
538
     * @inheritdoc
539
     */
540
    public function beforeDelete(): bool
541
    {
542
        // Delete field content table
543
        MetaPlugin::getInstance()->getConfiguration()->beforeDelete($this);
544
545
        // Trigger a 'beforeDelete' event
546
        return parent::beforeDelete();
547
    }
548
549
    /**
550
     * @inheritdoc
551
     */
552
    public function afterElementSave(ElementInterface $element, bool $isNew)
553
    {
554
        // Save meta element
555
        MetaPlugin::getInstance()->getField()->afterElementSave($this, $element);
556
557
        // Trigger an 'afterElementSave' event
558
        parent::afterElementSave($element, $isNew);
559
    }
560
561
    /**
562
     * @inheritdoc
563
     */
564
    public function beforeElementDelete(ElementInterface $element): bool
565
    {
566
        // Delete meta elements
567
        if (!MetaPlugin::getInstance()->getField()->beforeElementDelete($this, $element)) {
568
            return false;
569
        }
570
571
        return parent::beforeElementDelete($element);
572
    }
573
574
    /**
575
     * @inheritdoc
576
     */
577
    protected function isValueEmpty($value, ElementInterface $element): bool
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...
578
    {
579
        /** @var MetaQuery $value */
580
        return $value->count() === 0;
581
    }
582
583
    /**
584
     *
585
     * Returns info about each field type for the configurator.
586
     *
587
     * @return array
588
     */
589
    private function getFieldOptionsForConfiguration()
590
    {
591
        $disallowedFields = [
592
            self::class,
593
            Matrix::class
594
        ];
595
596
        $fieldTypes = [];
597
598
        // Set a temporary namespace for these
599
        $originalNamespace = Craft::$app->getView()->getNamespace();
600
        $namespace = Craft::$app->getView()->namespaceInputName('fields[__META_FIELD__][settings]', $originalNamespace);
601
        Craft::$app->getView()->setNamespace($namespace);
602
603
        /** @var Field|string $class */
604
        foreach (Craft::$app->getFields()->getAllFieldTypes() as $class) {
605
            // Ignore disallowed fields
606
            if (in_array($class, $disallowedFields)) {
607
                continue;
608
            }
609
610
            Craft::$app->getView()->startJsBuffer();
611
612
            /** @var FieldInterface $field */
613
            $field = new $class();
614
615
            if ($settingsHtml = (string)$field->getSettingsHtml()) {
616
                $settingsHtml = Craft::$app->getView()->namespaceInputs($settingsHtml);
617
            }
618
619
            $settingsBodyHtml = $settingsHtml;
620
            $settingsFootHtml = Craft::$app->getView()->clearJsBuffer();
621
622
            $fieldTypes[] = [
623
                'type' => $class,
624
                'name' => $class::displayName(),
625
                'settingsBodyHtml' => $settingsBodyHtml,
626
                'settingsFootHtml' => $settingsFootHtml,
627
            ];
628
        }
629
630
        Craft::$app->getView()->setNamespace($originalNamespace);
631
632
        return $fieldTypes;
633
    }
634
635
    /**
636
     * Returns html for all associated field types for the Meta field input.
637
     *
638
     * @return array
639
     */
640
    private function getFieldInfoForInput(): array
641
    {
642
        // Set a temporary namespace for these
643
        $originalNamespace = Craft::$app->getView()->getNamespace();
644
        $namespace = Craft::$app->getView()->namespaceInputName(
645
            $this->handle . '[__META__][fields]',
646
            $originalNamespace
647
        );
648
        Craft::$app->getView()->setNamespace($namespace);
649
650
        $fieldLayoutFields = $this->getFields();
651
652
        // Set $_isFresh's
653
        foreach ($fieldLayoutFields as $field) {
654
            $field->setIsFresh(true);
655
        }
656
657
        Craft::$app->getView()->startJsBuffer();
658
659
        $bodyHtml = Craft::$app->getView()->namespaceInputs(
660
            Craft::$app->getView()->renderTemplate(
661
                '_includes/fields',
662
                [
663
                    'namespace' => null,
664
                    'fields' => $fieldLayoutFields
665
                ]
666
            )
667
        );
668
669
        // Reset $_isFresh's
670
        foreach ($fieldLayoutFields as $field) {
671
            $field->setIsFresh(null);
672
        }
673
674
        $footHtml = Craft::$app->getView()->clearJsBuffer();
675
676
        $fields = [
677
            'bodyHtml' => $bodyHtml,
678
            'footHtml' => $footHtml,
679
        ];
680
681
        // Revert namespace
682
        Craft::$app->getView()->setNamespace($originalNamespace);
683
684
        return $fields;
685
    }
686
687
    /**
688
     * Creates an array of elements based on the given serialized data.
689
     *
690
     * @param array|string $value The raw field value
691
     * @param ElementInterface|null $element The element the field is associated with, if there is one
692
     *
693
     * @return MetaElement[]
694
     */
695
    private function createElementsFromSerializedData($value, ElementInterface $element = null): array
696
    {
697
        /** @var Element $element */
698
699
        if (!is_array($value)) {
700
            return [];
701
        }
702
703
        $oldElementsById = [];
704
705
        // Get the old elements that are still around
706
        if (!empty($element->id)) {
707
            $ownerId = $element->id;
708
709
            $ids = [];
710
711
            foreach ($value as $metaId => &$meta) {
712
                if (is_numeric($metaId) && $metaId !== 0) {
713
                    $ids[] = $metaId;
714
                }
715
            }
716
            unset($meta);
717
718
            if (!empty($ids)) {
719
                $oldMetaQuery = MetaElement::find();
720
                $oldMetaQuery->fieldId($this->id);
721
                $oldMetaQuery->ownerId($ownerId);
722
                $oldMetaQuery->id($ids);
723
                $oldMetaQuery->limit(null);
724
                $oldMetaQuery->status(null);
725
                $oldMetaQuery->enabledForSite(false);
726
                $oldMetaQuery->siteId($element->siteId);
727
                $oldMetaQuery->indexBy('id');
728
                $oldElementsById = $oldMetaQuery->all();
729
            }
730
        } else {
731
            $ownerId = null;
732
        }
733
734
        $elements = [];
735
        $sortOrder = 0;
736
        $prevElement = null;
737
738
        foreach ($value as $metaId => $metaData) {
739
            // Is this new? (Or has it been deleted?)
740
            if (strpos($metaId, 'new') === 0 || !isset($oldElementsById[$metaId])) {
741
                $meta = new MetaElement();
742
                $meta->fieldId = $this->id;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->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...
743
                $meta->ownerId = $ownerId;
744
                $meta->siteId = $element->siteId;
745
            } else {
746
                $meta = $oldElementsById[$metaId];
747
            }
748
749
            $meta->setOwner($element);
750
            $meta->enabled = (isset($metaData['enabled']) ? (bool)$metaData['enabled'] : true);
751
752
            // Set the content post location on the element if we can
753
            $fieldNamespace = $element->getFieldParamNamespace();
754
755
            if ($fieldNamespace !== null) {
756
                $metaFieldNamespace = ($fieldNamespace ? $fieldNamespace . '.' : '') .
757
                    '.' . $this->handle .
758
                    '.' . $metaId .
759
                    '.fields';
760
                $meta->setFieldParamNamespace($metaFieldNamespace);
761
            }
762
763
            if (isset($metaData['fields'])) {
764
                $meta->setFieldValues($metaData['fields']);
765
            }
766
767
            $sortOrder++;
768
            $meta->sortOrder = $sortOrder;
769
770
            // Set the prev/next elements
771
            if ($prevElement) {
772
                /** @var ElementInterface $prevElement */
773
                $prevElement->setNext($meta);
774
                /** @var ElementInterface $meta */
775
                $meta->setPrev($prevElement);
776
            }
777
            $prevElement = $meta;
778
779
            $elements[] = $meta;
780
        }
781
782
        return $elements;
783
    }
784
785
    /**
786
     * Returns the fields associated with this element.
787
     *
788
     * @return FieldInterface[]
789
     */
790
    public function getFields(): array
791
    {
792
        return $this->getFieldLayout()->getFields();
793
    }
794
795
    /**
796
     * Sets the fields associated with this element.
797
     *
798
     * @param FieldInterface[] $fields
799
     *
800
     * @return void
801
     */
802
    public function setFields(array $fields)
803
    {
804
        $defaultFieldConfig = [
805
            'type' => null,
806
            'name' => null,
807
            'handle' => null,
808
            'instructions' => null,
809
            'required' => false,
810
            'translationMethod' => Field::TRANSLATION_METHOD_NONE,
811
            'translationKeyFormat' => null,
812
            'settings' => null,
813
        ];
814
815
        foreach ($fields as $fieldId => $fieldConfig) {
816
            if (!$fieldConfig instanceof FieldInterface) {
817
818
                /** @noinspection SlowArrayOperationsInLoopInspection */
819
                $fieldConfig = array_merge($defaultFieldConfig, $fieldConfig);
820
821
                $fields[$fieldId] = Craft::$app->getFields()->createField([
822
                    'type' => $fieldConfig['type'],
823
                    'id' => $fieldId,
824
                    'name' => $fieldConfig['name'],
825
                    'handle' => $fieldConfig['handle'],
826
                    'instructions' => $fieldConfig['instructions'],
827
                    'required' => (bool)$fieldConfig['required'],
828
                    'translationMethod' => $fieldConfig['translationMethod'],
829
                    'translationKeyFormat' => $fieldConfig['translationKeyFormat'],
830
                    'settings' => $fieldConfig['settings'],
831
                ]);
832
            }
833
        }
834
835
        $this->getFieldLayout()->setFields($fields);
836
    }
837
}
838