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) { |
|
|
|
|
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
|
|
|
|
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 thecomposer.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
orrequire-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 you have not tested against this specific condition, such errors might go unnoticed.