Completed
Push — master ( 07eadb...49cf5c )
by Bart
04:28
created

Fields::getPrepareFieldLayout()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 21
ccs 0
cts 15
cp 0
rs 9.0534
cc 4
eloc 12
nc 4
nop 1
crap 20
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-2016, Nerds & Company
20
 * @license   MIT
21
 *
22
 * @link      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
    //===============================================  SERVICES  ===================================================
51
    //==============================================================================================================
52
53
    /**
54
     * Returns fields service.
55
     *
56
     * @return FieldsService
57
     */
58
    private function getFieldsService()
59
    {
60
        return Craft::app()->fields;
61
    }
62
63
    /**
64
     * Returns content service.
65
     *
66
     * @return ContentService
67
     */
68
    private function getContentService()
69
    {
70
        return Craft::app()->content;
71
    }
72
73
    //==============================================================================================================
74
    //================================================  EXPORT  ====================================================
75
    //==============================================================================================================
76
77
    /**
78
     * Export fields.
79
     *
80
     * @param FieldGroupModel[] $groups
81
     *
82
     * @return array
83
     */
84
    public function export(array $groups = [])
85
    {
86
        Craft::log(Craft::t('Exporting Fields'));
87
88
        $groupDefinitions = [];
89
90
        foreach ($groups as $group) {
91
            $fieldDefinitions = [];
92
93
            foreach ($group->getFields() as $field) {
94
                $fieldDefinitions[$field->handle] = $this->getFieldDefinition($field);
95
            }
96
97
            $groupDefinitions[$group->name] = $fieldDefinitions;
98
        }
99
100
        return $groupDefinitions;
101
    }
102
103
    /**
104
     * Get field definition.
105
     *
106
     * @param FieldModel $field
107
     *
108
     * @return array
109
     */
110
    private function getFieldDefinition(FieldModel $field)
111
    {
112
        $fieldFactory = $this->getFieldFactory();
113
        $schematicFieldModel = $fieldFactory->build($field->type);
114
        $definition = $schematicFieldModel->getDefinition($field, true);
115
116
        return $definition;
117
    }
118
119
    //==============================================================================================================
120
    //================================================  IMPORT  ====================================================
121
    //==============================================================================================================
122
123
    /**
124
     * Attempt to import fields.
125
     *
126
     * @param array $groupDefinitions
127
     * @param bool  $force            if set to true items not in the import will be deleted
128
     *
129
     * @return Result
130
     */
131
    public function import(array $groupDefinitions, $force = false)
132
    {
133
        Craft::log(Craft::t('Importing Fields'));
134
135
        if (!empty($groupDefinitions)) {
136
            $contentService = $this->getContentService();
137
138
            $contentService->fieldContext = 'global';
139
            $contentService->contentTable = 'content';
140
141
            $this->resetCraftFieldsServiceGroupsCache();
142
            $this->groups = $this->getFieldsService()->getAllGroups('name');
143
            $this->fields = $this->getFieldsService()->getAllFields('handle');
144
145
            foreach ($groupDefinitions as $name => $fieldDefinitions) {
146
                try {
147
                    $this->beginTransaction();
148
149
                    $group = $this->createFieldGroupModel($name);
150
151
                    $this->importFields($fieldDefinitions, $group);
152
153
                    $this->commitTransaction();
154
                } catch (\Exception $e) {
155
                    $this->rollbackTransaction();
156
157
                    $this->addError($e->getMessage());
158
                }
159
160
                $this->unsetData($name, $fieldDefinitions);
161
            }
162
163
            if ($force) { // Remove not imported data
164
                $this->deleteFieldsAndGroups();
165
            }
166
            $this->resetCraftFieldsServiceFieldsCache();
167
        }
168
169
        return $this->getResultModel();
170
    }
171
172
    /**
173
     * Save field group.
174
     *
175
     * @param FieldGroupModel $group
176
     *
177
     * @throws Exception
178
     */
179
    private function saveFieldGroupModel(FieldGroupModel $group)
180
    {
181
        if (!$this->getFieldsService()->saveGroup($group)) {
182
            $this->addErrors($group->getAllErrors());
183
184
            throw new Exception('Failed to save group');
185
        }
186
    }
187
188
    /**
189
     * Save field.
190
     *
191
     * @param FieldModel $field
192
     *
193
     * @throws \Exception
194
     */
195
    private function saveFieldModel(FieldModel $field)
196
    {
197
        $this->validateFieldModel($field); // Validate field
198
        if (!$this->getFieldsService()->saveField($field)) {
199
            $this->addErrors($field->getAllErrors());
200
201
            throw new Exception('Failed to save field');
202
        }
203
    }
204
205
    /**
206
     * Removes fields that where not imported.
207
     */
208
    private function deleteFields()
209
    {
210
        $fieldsService = $this->getFieldsService();
211
        foreach ($this->fields as $field) {
212
            $fieldsService->deleteFieldById($field->id);
213
        }
214
    }
215
216
    /**
217
     * Removes groups that where not imported.
218
     */
219
    private function deleteGroups()
220
    {
221
        $fieldsService = $this->getFieldsService();
222
        foreach ($this->groups as $group) {
223
            $fieldsService->deleteGroupById($group->id);
224
        }
225
    }
226
227
    /**
228
     * Removes fields and groups that where not imported.
229
     */
230
    private function deleteFieldsAndGroups()
231
    {
232
        $this->deleteFields();
233
        $this->deleteGroups();
234
    }
235
236
    /**
237
     * Creates new or updates existing group model.
238
     *
239
     * @param string $group
240
     *
241
     * @return FieldGroupModel
242
     */
243
    private function createFieldGroupModel($group)
244
    {
245
        $groupModel = (array_key_exists($group, $this->groups) ? $this->groups[$group] : new FieldGroupModel());
246
        $groupModel->name = $group;
247
248
        $this->saveFieldGroupModel($groupModel);
249
250
        return $groupModel;
251
    }
252
253
    /**
254
     * @param string $field
255
     *
256
     * @return FieldModel
257
     */
258
    private function getFieldModel($field)
259
    {
260
        return (array_key_exists($field, $this->fields) ? $this->fields[$field] : new FieldModel());
261
    }
262
263
    /**
264
     * Validates field type, throw error when it's incorrect.
265
     *
266
     * @param FieldModel $field
267
     *
268
     * @throws \Exception
269
     */
270
    private function validateFieldModel(FieldModel $field)
271
    {
272
        if (!$field->getFieldType()) {
273
            $fieldType = $field->type;
274
            ($fieldType == 'Matrix')
275
                ? $this->addError("One of the field's types does not exist. Are you missing a plugin?")
276
                : $this->addError("Field type '$fieldType' does not exist. Are you missing a plugin?");
277
278
            throw new Exception('Failed to save field');
279
        }
280
    }
281
282
    /**
283
     * Import field group fields.
284
     *
285
     * @param array           $fieldDefinitions
286
     * @param FieldGroupModel $group
287
     *
288
     * @throws \Exception
289
     */
290
    private function importFields(array $fieldDefinitions, FieldGroupModel $group)
291
    {
292
        $fieldFactory = $this->getFieldFactory();
293
294
        foreach ($fieldDefinitions as $fieldHandle => $fieldDef) {
295
            $field = $this->getFieldModel($fieldHandle);
296
            $schematicFieldModel = $fieldFactory->build($fieldDef['type']);
297
298
            if ($schematicFieldModel->getDefinition($field, true) === $fieldDef) {
299
                Craft::log(Craft::t('Skipping `{name}`, no changes detected', ['name' => $field->name]));
300
                continue;
301
            }
302
303
            Craft::log(Craft::t('Importing `{name}`', ['name' => $fieldDef['name']]));
304
305
            $schematicFieldModel->populate($fieldDef, $field, $fieldHandle, $group);
306
            $this->saveFieldModel($field);
307
        }
308
    }
309
310
    /**
311
     * Unset group and field data else $force flag will delete it.
312
     *
313
     * @param string $name
314
     * @param array  $definitions
315
     */
316
    private function unsetData($name, array $definitions)
317
    {
318
        if (array_key_exists($name, $this->groups)) {
319
            unset($this->groups[$name]);
320
            foreach ($definitions as $handle => $definition) {
321
                unset($this->fields[$handle]);
322
            }
323
        }
324
    }
325
326
    //==============================================================================================================
327
    //=============================================  FIELD LAYOUT  =================================================
328
    //==============================================================================================================
329
330
    /**
331
     * Get field layout definition.
332
     *
333
     * @param FieldLayoutModel $fieldLayout
334
     *
335
     * @return array
336
     */
337
    public function getFieldLayoutDefinition(FieldLayoutModel $fieldLayout)
338
    {
339
        if ($fieldLayout->getTabs()) {
340
            $tabDefinitions = [];
341
342
            foreach ($fieldLayout->getTabs() as $tab) {
343
                $tabDefinitions[$tab->name] = $this->getFieldLayoutFieldsDefinition($tab->getFields());
344
            }
345
346
            return ['tabs' => $tabDefinitions];
347
        }
348
349
        return ['fields' => $this->getFieldLayoutFieldsDefinition($fieldLayout->getFields())];
350
    }
351
352
    /**
353
     * Get field layout fields definition.
354
     *
355
     * @param FieldLayoutFieldModel[] $fields
356
     *
357
     * @return array
358
     */
359
    private function getFieldLayoutFieldsDefinition(array $fields)
360
    {
361
        $fieldDefinitions = [];
362
363
        foreach ($fields as $field) {
364
            $fieldDefinitions[$field->getField()->handle] = $field->required;
365
        }
366
367
        return $fieldDefinitions;
368
    }
369
370
    /**
371
     * Attempt to import a field layout.
372
     *
373
     * @param array $fieldLayoutDef
374
     *
375
     * @return FieldLayoutModel
376
     */
377
    public function getFieldLayout(array $fieldLayoutDef)
378
    {
379
        $layoutFields = [];
380
        $requiredFields = [];
381
382
        if (array_key_exists('tabs', $fieldLayoutDef)) {
383
            foreach ($fieldLayoutDef['tabs'] as $tabName => $tabDef) {
384
                $layoutTabFields = $this->getPrepareFieldLayout($tabDef);
385
                $requiredFields = array_merge($requiredFields, $layoutTabFields['required']);
386
                $layoutFields[$tabName] = $layoutTabFields['fields'];
387
            }
388
        } elseif (array_key_exists('fields', $fieldLayoutDef)) {
389
            $layoutTabFields = $this->getPrepareFieldLayout($fieldLayoutDef);
390
            $requiredFields = $layoutTabFields['required'];
391
            $layoutFields = $layoutTabFields['fields'];
392
        }
393
394
        $fieldLayout = Craft::app()->fields->assembleLayout($layoutFields, $requiredFields);
395
        $fieldLayout->type = ElementType::Entry;
396
397
        return $fieldLayout;
398
    }
399
400
    /**
401
     * Get a prepared fieldLayout for the craft assembleLayout function.
402
     *
403
     * @param array $fieldLayoutDef
404
     *
405
     * @return array
406
     */
407
    private function getPrepareFieldLayout(array $fieldLayoutDef)
408
    {
409
        $layoutFields = [];
410
        $requiredFields = [];
411
412
        foreach ($fieldLayoutDef as $fieldHandle => $required) {
413
            $field = Craft::app()->fields->getFieldByHandle($fieldHandle);
414
            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...
415
                $layoutFields[] = $field->id;
416
417
                if ($required) {
418
                    $requiredFields[] = $field->id;
419
                }
420
            }
421
        }
422
423
        return [
424
            'fields' => $layoutFields,
425
            'required' => $requiredFields,
426
        ];
427
    }
428
429
    /**
430
     * Reset craft fields service groups cache using reflection
431
     */
432
    private function resetCraftFieldsServiceGroupsCache()
433
    {
434
        $obj         = $this->getFieldsService();
435
        $refObject   = new \ReflectionObject($obj);
436
        $refProperty = $refObject->getProperty('_fetchedAllGroups');
437
        $refProperty->setAccessible(true);
438
        $refProperty->setValue($obj, false);
439
    }
440
441
    /**
442
     * Reset craft fields service fields cache using reflection
443
     */
444
    private function resetCraftFieldsServiceFieldsCache()
445
    {
446
        $obj         = $this->getFieldsService();
447
        $refObject   = new \ReflectionObject($obj);
448
        $refProperty = $refObject->getProperty('_fieldsByContextAndHandle');
449
        $refProperty->setAccessible(true);
450
        $refProperty->setValue($obj, array());
451
    }
452
}
453