Completed
Pull Request — master (#107)
by Bart
09:07
created

Fields::getFieldDefinition()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
ccs 0
cts 5
cp 0
cc 1
eloc 5
nc 1
nop 1
crap 2
1
<?php
2
3
namespace NerdsAndCompany\Schematic\Services;
4
5
use Craft\Craft;
6
use Craft\Exception;
7
use Craft\FieldModel;
8
use Craft\FieldGroupModel;
9
use Craft\FieldLayoutModel;
10
use Craft\ElementType;
11
use NerdsAndCompany\Schematic\Models\FieldFactory;
12
13
/**
14
 * Schematic Fields Service.
15
 *
16
 * Sync Craft Setups.
17
 *
18
 * @author    Nerds & Company
19
 * @copyright Copyright (c) 2015-2017, Nerds & Company
20
 * @license   MIT
21
 *
22
 * @see      http://www.nerds.company
23
 */
24
class Fields extends Base
25
{
26
    /**
27
     * @var FieldModel[]
28
     */
29
    private $fields = [];
30
31
    /**
32
     * @var FieldGroupModel[]
33
     */
34
    private $groups = [];
35
36
    /**
37
     * @var FieldFactory
38
     */
39
    private $fieldFactory;
40
41
    /**
42
     * @return FieldFactory
43
     */
44
    public function getFieldFactory()
45
    {
46
        return isset($this->fieldFactory) ? $this->fieldFactory : new FieldFactory();
47
    }
48
49
    //==============================================================================================================
50
    //================================================  EXPORT  ====================================================
51
    //==============================================================================================================
52
53
    /**
54
     * Export fields.
55
     *
56
     * @param FieldGroupModel[] $groups
57
     *
58
     * @return array
59
     */
60
    public function export(array $groups = [])
61
    {
62
        Craft::log(Craft::t('Exporting Fields'));
63
64
        $groupDefinitions = [];
65
66
        foreach ($groups as $group) {
67
            $fieldDefinitions = [];
68
69
            foreach ($group->getFields() as $field) {
70
                $fieldDefinitions[$field->handle] = $this->getFieldDefinition($field);
71
            }
72
73
            $groupDefinitions[$group->name] = $fieldDefinitions;
74
        }
75
76
        return $groupDefinitions;
77
    }
78
79
    /**
80
     * Get field definition.
81
     *
82
     * @param FieldModel $field
83
     *
84
     * @return array
85
     */
86
    private function getFieldDefinition(FieldModel $field)
87
    {
88
        $fieldFactory = $this->getFieldFactory();
89
        $schematicFieldModel = $fieldFactory->build($field->type);
90
        $definition = $schematicFieldModel->getDefinition($field, true);
91
92
        return $definition;
93
    }
94
95
    //==============================================================================================================
96
    //================================================  IMPORT  ====================================================
97
    //==============================================================================================================
98
99
    /**
100
     * Attempt to import fields.
101
     *
102
     * @param array $groupDefinitions
103
     * @param bool  $force            if set to true items not in the import will be deleted
104
     *
105
     * @return Result
106
     */
107
    public function import(array $groupDefinitions, $force = false)
108
    {
109
        Craft::log(Craft::t('Importing Fields'));
110
111
        if (!empty($groupDefinitions)) {
112
            $this->setGlobalContext();
113
            $this->resetCraftFieldsServiceGroupsCache();
114
            $this->groups = Craft::app()->fields->getAllGroups('name');
115
            $this->fields = Craft::app()->fields->getAllFields('handle');
116
117
            foreach ($groupDefinitions as $name => $fieldDefinitions) {
118
                try {
119
                    $this->beginTransaction();
120
121
                    $group = $this->createFieldGroupModel($name);
122
123
                    $this->importFields($fieldDefinitions, $group, $force);
124
125
                    $this->commitTransaction();
126
                } catch (\Exception $e) {
127
                    $this->rollbackTransaction();
128
129
                    $this->addError($e->getMessage());
130
                }
131
132
                $this->unsetData($name, $fieldDefinitions);
133
            }
134
135
            if ($force) { // Remove not imported data
136
                $this->deleteFieldsAndGroups();
137
            }
138
            $this->resetCraftFieldsServiceFieldsCache();
139
        }
140
141
        return $this->getResultModel();
142
    }
143
144
    /**
145
     * Save field group.
146
     *
147
     * @param FieldGroupModel $group
148
     *
149
     * @throws Exception
150
     */
151
    private function saveFieldGroupModel(FieldGroupModel $group)
152
    {
153
        if (!Craft::app()->fields->saveGroup($group)) {
154
            $this->addErrors($group->getAllErrors());
155
156
            throw new Exception('Failed to save group');
157
        }
158
    }
159
160
    /**
161
     * Save field.
162
     *
163
     * @param FieldModel $field
164
     *
165
     * @throws \Exception
166
     */
167
    private function saveFieldModel(FieldModel $field)
168
    {
169
        $this->validateFieldModel($field); // Validate field
170
        if ($field->context === 'global') {
171
            $this->setGlobalContext();
172
        }
173
        if (!Craft::app()->fields->saveField($field)) {
174
            $this->addErrors($field->getAllErrors());
175
176
            throw new Exception('Failed to save field');
177
        }
178
    }
179
180
    /**
181
     * Removes fields that where not imported.
182
     */
183
    private function deleteFields()
184
    {
185
        $fieldsService = Craft::app()->fields;
186
        foreach ($this->fields as $field) {
187
            $fieldsService->deleteFieldById($field->id);
188
        }
189
    }
190
191
    /**
192
     * Removes groups that where not imported.
193
     */
194
    private function deleteGroups()
195
    {
196
        $fieldsService = Craft::app()->fields;
197
        foreach ($this->groups as $group) {
198
            $fieldsService->deleteGroupById($group->id);
199
        }
200
    }
201
202
    /**
203
     * Removes fields and groups that where not imported.
204
     */
205
    private function deleteFieldsAndGroups()
206
    {
207
        $this->deleteFields();
208
        $this->deleteGroups();
209
    }
210
211
    /**
212
     * Creates new or updates existing group model.
213
     *
214
     * @param string $group
215
     *
216
     * @return FieldGroupModel
217
     */
218
    private function createFieldGroupModel($group)
219
    {
220
        $groupModel = (array_key_exists($group, $this->groups) ? $this->groups[$group] : new FieldGroupModel());
221
        $groupModel->name = $group;
222
223
        $this->saveFieldGroupModel($groupModel);
224
225
        return $groupModel;
226
    }
227
228
    /**
229
     * @param string $field
230
     *
231
     * @return FieldModel
232
     */
233
    private function getFieldModel($field)
234
    {
235
        return array_key_exists($field, $this->fields) ? $this->fields[$field] : new FieldModel();
236
    }
237
238
    /**
239
     * Validates field type, throw error when it's incorrect.
240
     *
241
     * @param FieldModel $field
242
     *
243
     * @throws \Exception
244
     */
245
    private function validateFieldModel(FieldModel $field)
246
    {
247
        if (!$field->getFieldType()) {
248
            $fieldType = $field->type;
249
            ($fieldType == 'Matrix')
250
                ? $this->addError("One of the field's types does not exist. Are you missing a plugin?")
251
                : $this->addError("Field type '$fieldType' does not exist. Are you missing a plugin?");
252
253
            throw new Exception('Failed to save field');
254
        }
255
    }
256
257
    /**
258
     * Import field group fields.
259
     *
260
     * @param array           $fieldDefinitions
261
     * @param FieldGroupModel $group
262
     * @param bool            $force
263
     *
264
     * @throws \Exception
265
     */
266
    private function importFields(array $fieldDefinitions, FieldGroupModel $group, $force = false)
267
    {
268
        $fieldFactory = $this->getFieldFactory();
269
270
        foreach ($fieldDefinitions as $fieldHandle => $fieldDef) {
271
            $field = $this->getFieldModel($fieldHandle);
272
            $schematicFieldModel = $fieldFactory->build($fieldDef['type']);
273
274
            if ($schematicFieldModel->getDefinition($field, true) === $fieldDef) {
275
                Craft::log(Craft::t('Skipping `{name}`, no changes detected', ['name' => $field->name]));
276
                continue;
277
            }
278
279
            Craft::log(Craft::t('Importing `{name}`', ['name' => $fieldDef['name']]));
280
281
            $schematicFieldModel->populate($fieldDef, $field, $fieldHandle, $group, $force);
282
            $this->saveFieldModel($field);
283
        }
284
    }
285
286
    /**
287
     * Unset group and field data else $force flag will delete it.
288
     *
289
     * @param string $name
290
     * @param array  $definitions
291
     */
292
    private function unsetData($name, array $definitions)
293
    {
294
        if (array_key_exists($name, $this->groups)) {
295
            unset($this->groups[$name]);
296
            foreach ($definitions as $handle => $definition) {
297
                unset($this->fields[$handle]);
298
            }
299
        }
300
    }
301
302
    /**
303
     * Set global field context
304
     */
305
    private function setGlobalContext()
306
    {
307
      Craft::app()->content->fieldContext = 'global';
308
      Craft::app()->content->contentTable = 'content';
309
    }
310
311
    //==============================================================================================================
312
    //=============================================  FIELD LAYOUT  =================================================
313
    //==============================================================================================================
314
315
    /**
316
     * Get field layout definition.
317
     *
318
     * @param FieldLayoutModel $fieldLayout
319
     *
320
     * @return array
321
     */
322
    public function getFieldLayoutDefinition(FieldLayoutModel $fieldLayout)
323
    {
324
        if ($fieldLayout->getTabs()) {
325
            $tabDefinitions = [];
326
327
            foreach ($fieldLayout->getTabs() as $tab) {
328
                $tabDefinitions[$tab->name] = $this->getFieldLayoutFieldsDefinition($tab->getFields());
329
            }
330
331
            return ['tabs' => $tabDefinitions];
332
        }
333
334
        return ['fields' => $this->getFieldLayoutFieldsDefinition($fieldLayout->getFields())];
335
    }
336
337
    /**
338
     * Get field layout fields definition.
339
     *
340
     * @param FieldLayoutFieldModel[] $fields
341
     *
342
     * @return array
343
     */
344
    private function getFieldLayoutFieldsDefinition(array $fields)
345
    {
346
        $fieldDefinitions = [];
347
348
        foreach ($fields as $field) {
349
            $fieldDefinitions[$field->getField()->handle] = $field->required;
350
        }
351
352
        return $fieldDefinitions;
353
    }
354
355
    /**
356
     * Attempt to import a field layout.
357
     *
358
     * @param array $fieldLayoutDef
359
     *
360
     * @return FieldLayoutModel
361
     */
362
    public function getFieldLayout(array $fieldLayoutDef)
363
    {
364
        $layoutFields = [];
365
        $requiredFields = [];
366
367
        if (array_key_exists('tabs', $fieldLayoutDef)) {
368
            foreach ($fieldLayoutDef['tabs'] as $tabName => $tabDef) {
369
                $layoutTabFields = $this->getPrepareFieldLayout($tabDef);
370
                $requiredFields = array_merge($requiredFields, $layoutTabFields['required']);
371
                $layoutFields[$tabName] = $layoutTabFields['fields'];
372
            }
373
        } elseif (array_key_exists('fields', $fieldLayoutDef)) {
374
            $layoutTabFields = $this->getPrepareFieldLayout($fieldLayoutDef);
375
            $requiredFields = $layoutTabFields['required'];
376
            $layoutFields = $layoutTabFields['fields'];
377
        }
378
379
        $fieldLayout = Craft::app()->fields->assembleLayout($layoutFields, $requiredFields);
380
        $fieldLayout->type = ElementType::Entry;
381
382
        return $fieldLayout;
383
    }
384
385
    /**
386
     * Get a prepared fieldLayout for the craft assembleLayout function.
387
     *
388
     * @param array $fieldLayoutDef
389
     *
390
     * @return array
391
     */
392
    private function getPrepareFieldLayout(array $fieldLayoutDef)
393
    {
394
        $layoutFields = [];
395
        $requiredFields = [];
396
397
        foreach ($fieldLayoutDef as $fieldHandle => $required) {
398
            $field = Craft::app()->fields->getFieldByHandle($fieldHandle);
399
            if ($field instanceof FieldModel) {
0 ignored issues
show
Bug introduced by
The class Craft\FieldModel does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
400
                $layoutFields[] = $field->id;
401
402
                if ($required) {
403
                    $requiredFields[] = $field->id;
404
                }
405
            }
406
        }
407
408
        return [
409
            'fields' => $layoutFields,
410
            'required' => $requiredFields,
411
        ];
412
    }
413
414
    /**
415
     * Reset craft fields service groups cache using reflection.
416
     */
417
    private function resetCraftFieldsServiceGroupsCache()
418
    {
419
        $obj = Craft::app()->fields;
420
        $refObject = new \ReflectionObject($obj);
421
        $refProperty = $refObject->getProperty('_fetchedAllGroups');
422
        $refProperty->setAccessible(true);
423
        $refProperty->setValue($obj, false);
424
    }
425
426
    /**
427
     * Reset craft fields service fields cache using reflection.
428
     */
429
    private function resetCraftFieldsServiceFieldsCache()
430
    {
431
        $obj = Craft::app()->fields;
432
        $refObject = new \ReflectionObject($obj);
433
        $refProperty1 = $refObject->getProperty('_allFieldsInContext');
434
        $refProperty1->setAccessible(true);
435
        $refProperty1->setValue($obj, array());
436
        $refProperty2 = $refObject->getProperty('_fieldsByContextAndHandle');
437
        $refProperty2->setAccessible(true);
438
        $refProperty2->setValue($obj, array());
439
    }
440
}
441