Completed
Push — master ( 2c7560...1eea38 )
by Nate
01:32
created

Configuration::getInputHtml()   B

Complexity

Conditions 4
Paths 2

Size

Total Lines 43
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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