Completed
Pull Request — master (#111)
by Bob Olde
02:17
created

Fields::resetCraftFieldsServiceGroupsCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 0
cts 6
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 6
nc 1
nop 0
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
        $this->resetCraftDbSchemaContentTableCache();
190
    }
191
192
    /**
193
     * Removes groups that where not imported.
194
     */
195
    private function deleteGroups()
196
    {
197
        $fieldsService = Craft::app()->fields;
198
        foreach ($this->groups as $group) {
199
            $fieldsService->deleteGroupById($group->id);
200
        }
201
    }
202
203
    /**
204
     * Removes fields and groups that where not imported.
205
     */
206
    private function deleteFieldsAndGroups()
207
    {
208
        $this->deleteFields();
209
        $this->deleteGroups();
210
    }
211
212
    /**
213
     * Creates new or updates existing group model.
214
     *
215
     * @param string $group
216
     *
217
     * @return FieldGroupModel
218
     */
219
    private function createFieldGroupModel($group)
220
    {
221
        $groupModel = (array_key_exists($group, $this->groups) ? $this->groups[$group] : new FieldGroupModel());
222
        $groupModel->name = $group;
223
224
        $this->saveFieldGroupModel($groupModel);
225
226
        return $groupModel;
227
    }
228
229
    /**
230
     * @param string $field
231
     *
232
     * @return FieldModel
233
     */
234
    private function getFieldModel($field)
235
    {
236
        return array_key_exists($field, $this->fields) ? $this->fields[$field] : new FieldModel();
237
    }
238
239
    /**
240
     * Validates field type, throw error when it's incorrect.
241
     *
242
     * @param FieldModel $field
243
     *
244
     * @throws \Exception
245
     */
246
    private function validateFieldModel(FieldModel $field)
247
    {
248
        if (!$field->getFieldType()) {
249
            $fieldType = $field->type;
250
            ($fieldType == 'Matrix')
251
                ? $this->addError("One of the field's types does not exist. Are you missing a plugin?")
252
                : $this->addError("Field type '$fieldType' does not exist. Are you missing a plugin?");
253
254
            throw new Exception('Failed to save field');
255
        }
256
    }
257
258
    /**
259
     * Import field group fields.
260
     *
261
     * @param array           $fieldDefinitions
262
     * @param FieldGroupModel $group
263
     * @param bool            $force
264
     *
265
     * @throws \Exception
266
     */
267
    private function importFields(array $fieldDefinitions, FieldGroupModel $group, $force = false)
268
    {
269
        $fieldFactory = $this->getFieldFactory();
270
271
        foreach ($fieldDefinitions as $fieldHandle => $fieldDef) {
272
            $field = $this->getFieldModel($fieldHandle);
273
            $schematicFieldModel = $fieldFactory->build($fieldDef['type']);
274
275
            if ($schematicFieldModel->getDefinition($field, true) === $fieldDef) {
276
                Craft::log(Craft::t('Skipping `{name}`, no changes detected', ['name' => $field->name]));
277
                continue;
278
            }
279
280
            Craft::log(Craft::t('Importing `{name}`', ['name' => $fieldDef['name']]));
281
282
            $schematicFieldModel->populate($fieldDef, $field, $fieldHandle, $group, $force);
283
            $this->saveFieldModel($field);
284
        }
285
    }
286
287
    /**
288
     * Unset group and field data else $force flag will delete it.
289
     *
290
     * @param string $name
291
     * @param array  $definitions
292
     */
293
    private function unsetData($name, array $definitions)
294
    {
295
        if (array_key_exists($name, $this->groups)) {
296
            unset($this->groups[$name]);
297
            foreach ($definitions as $handle => $definition) {
298
                unset($this->fields[$handle]);
299
            }
300
        }
301
    }
302
303
    /**
304
     * Set global field context.
305
     */
306
    private function setGlobalContext()
307
    {
308
        Craft::app()->content->fieldContext = 'global';
309
        Craft::app()->content->contentTable = 'content';
310
    }
311
312
    //==============================================================================================================
313
    //=============================================  FIELD LAYOUT  =================================================
314
    //==============================================================================================================
315
316
    /**
317
     * Get field layout definition.
318
     *
319
     * @param FieldLayoutModel $fieldLayout
320
     *
321
     * @return array
322
     */
323
    public function getFieldLayoutDefinition(FieldLayoutModel $fieldLayout)
324
    {
325
        if ($fieldLayout->getTabs()) {
326
            $tabDefinitions = [];
327
328
            foreach ($fieldLayout->getTabs() as $tab) {
329
                $tabDefinitions[$tab->name] = $this->getFieldLayoutFieldsDefinition($tab->getFields());
330
            }
331
332
            return ['tabs' => $tabDefinitions];
333
        }
334
335
        return ['fields' => $this->getFieldLayoutFieldsDefinition($fieldLayout->getFields())];
336
    }
337
338
    /**
339
     * Get field layout fields definition.
340
     *
341
     * @param FieldLayoutFieldModel[] $fields
342
     *
343
     * @return array
344
     */
345
    private function getFieldLayoutFieldsDefinition(array $fields)
346
    {
347
        $fieldDefinitions = [];
348
349
        foreach ($fields as $field) {
350
            $fieldDefinitions[$field->getField()->handle] = $field->required;
351
        }
352
353
        return $fieldDefinitions;
354
    }
355
356
    /**
357
     * Attempt to import a field layout.
358
     *
359
     * @param array $fieldLayoutDef
360
     *
361
     * @return FieldLayoutModel
362
     */
363
    public function getFieldLayout(array $fieldLayoutDef)
364
    {
365
        $layoutFields = [];
366
        $requiredFields = [];
367
368
        if (array_key_exists('tabs', $fieldLayoutDef)) {
369
            foreach ($fieldLayoutDef['tabs'] as $tabName => $tabDef) {
370
                $layoutTabFields = $this->getPrepareFieldLayout($tabDef);
371
                $requiredFields = array_merge($requiredFields, $layoutTabFields['required']);
372
                $layoutFields[$tabName] = $layoutTabFields['fields'];
373
            }
374
        } elseif (array_key_exists('fields', $fieldLayoutDef)) {
375
            $layoutTabFields = $this->getPrepareFieldLayout($fieldLayoutDef);
376
            $requiredFields = $layoutTabFields['required'];
377
            $layoutFields = $layoutTabFields['fields'];
378
        }
379
380
        $fieldLayout = Craft::app()->fields->assembleLayout($layoutFields, $requiredFields);
381
        $fieldLayout->type = ElementType::Entry;
382
383
        return $fieldLayout;
384
    }
385
386
    /**
387
     * Get a prepared fieldLayout for the craft assembleLayout function.
388
     *
389
     * @param array $fieldLayoutDef
390
     *
391
     * @return array
392
     */
393
    private function getPrepareFieldLayout(array $fieldLayoutDef)
394
    {
395
        $layoutFields = [];
396
        $requiredFields = [];
397
398
        foreach ($fieldLayoutDef as $fieldHandle => $required) {
399
            $field = Craft::app()->fields->getFieldByHandle($fieldHandle);
400
            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...
401
                $layoutFields[] = $field->id;
402
403
                if ($required) {
404
                    $requiredFields[] = $field->id;
405
                }
406
            }
407
        }
408
409
        return [
410
            'fields' => $layoutFields,
411
            'required' => $requiredFields,
412
        ];
413
    }
414
415
    /**
416
     * Reset craft fields service groups cache using reflection.
417
     */
418
    private function resetCraftFieldsServiceGroupsCache()
419
    {
420
        $obj = Craft::app()->fields;
421
        $refObject = new \ReflectionObject($obj);
422
        $refProperty = $refObject->getProperty('_fetchedAllGroups');
423
        $refProperty->setAccessible(true);
424
        $refProperty->setValue($obj, false);
425
    }
426
427
    /**
428
     * Reset craft fields service fields cache using reflection.
429
     */
430
    private function resetCraftFieldsServiceFieldsCache()
431
    {
432
        $obj = Craft::app()->fields;
433
        $refObject = new \ReflectionObject($obj);
434
        $refProperty1 = $refObject->getProperty('_allFieldsInContext');
435
        $refProperty1->setAccessible(true);
436
        $refProperty1->setValue($obj, array());
437
        $refProperty2 = $refObject->getProperty('_fieldsByContextAndHandle');
438
        $refProperty2->setAccessible(true);
439
        $refProperty2->setValue($obj, array());
440
    }
441
442
    /**
443
     * Reset craft db schema content table cache using reflection.
444
     */
445
    private function resetCraftDbSchemaContentTableCache()
446
    {
447
        $obj = Craft::app()->db->schema;
448
        $refObject = (new \ReflectionObject($obj))->getParentClass()->getParentClass();
449
        $refProperty = $refObject->getProperty('_tables');
450
        $refProperty->setAccessible(true);
451
        $refProperty->setValue($obj, array());
452
    }
453
}
454