Passed
Push — main ( 2a1ce4...db05ee )
by Gaetano
09:12
created

ContentManager::generateMigration()   D

Complexity

Conditions 13
Paths 268

Size

Total Lines 128
Code Lines 87

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 65
CRAP Score 13.2224

Importance

Changes 0
Metric Value
cc 13
eloc 87
nc 268
nop 3
dl 0
loc 128
ccs 65
cts 73
cp 0.8904
crap 13.2224
rs 4.2303
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Core\Executor;
4
5
use eZ\Publish\API\Repository\Values\Content\Content;
6
use eZ\Publish\API\Repository\Values\Content\ContentCreateStruct;
7
use eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct;
8
use eZ\Publish\API\Repository\Values\Content\VersionInfo;
9
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
10
use eZ\Publish\API\Repository\Values\ContentType\FieldDefinition;
11
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
12
use Kaliop\eZMigrationBundle\API\Collection\ContentCollection;
13
use Kaliop\eZMigrationBundle\API\EnumerableMatcherInterface;
14
use Kaliop\eZMigrationBundle\API\Exception\InvalidStepDefinitionException;
15
use Kaliop\eZMigrationBundle\API\Exception\MigrationBundleException;
16
use Kaliop\eZMigrationBundle\API\MigrationGeneratorInterface;
17
use Kaliop\eZMigrationBundle\Core\FieldHandlerManager;
18
use Kaliop\eZMigrationBundle\Core\Helper\SortConverter;
19
use Kaliop\eZMigrationBundle\Core\Matcher\ContentMatcher;
20
use Kaliop\eZMigrationBundle\Core\Matcher\ObjectStateGroupMatcher;
21
use Kaliop\eZMigrationBundle\Core\Matcher\ObjectStateMatcher;
22
use Kaliop\eZMigrationBundle\Core\Matcher\SectionMatcher;
23
use Kaliop\eZMigrationBundle\Core\Matcher\UserMatcher;
24
use JmesPath\Env as JmesPath;
25
26
/**
27
 * Handles content migrations.
28
 *
29
 * @todo add support for updating of content metadata
30
 */
31
class ContentManager extends RepositoryExecutor implements MigrationGeneratorInterface, EnumerableMatcherInterface
32
{
33
    protected $supportedStepTypes = array('content');
34
    protected $supportedActions = array('create', 'load', 'update', 'delete');
35
36
    protected $contentMatcher;
37
    protected $sectionMatcher;
38
    protected $userMatcher;
39
    protected $objectStateMatcher;
40
    protected $objectStateGroupMatcher;
41
    protected $fieldHandlerManager;
42
    protected $locationManager;
43
    protected $sortConverter;
44
45
    // these are not exported when generating a migration
46
    protected $ignoredStateGroupIdentifiers = array('ez_lock');
47
48 149
    public function __construct(
49
        ContentMatcher $contentMatcher,
50
        SectionMatcher $sectionMatcher,
51
        UserMatcher $userMatcher,
52
        ObjectStateMatcher $objectStateMatcher,
53
        ObjectStateGroupMatcher $objectStateGroupMatcher,
54
        FieldHandlerManager $fieldHandlerManager,
55
        LocationManager $locationManager,
56
        SortConverter $sortConverter
57
    ) {
58 149
        $this->contentMatcher = $contentMatcher;
59 149
        $this->sectionMatcher = $sectionMatcher;
60 149
        $this->userMatcher = $userMatcher;
61 149
        $this->objectStateMatcher = $objectStateMatcher;
62 149
        $this->objectStateGroupMatcher = $objectStateGroupMatcher;
63 149
        $this->fieldHandlerManager = $fieldHandlerManager;
64 149
        $this->locationManager = $locationManager;
65 149
        $this->sortConverter = $sortConverter;
66 149
    }
67
68
    /**
69
     * Handles the content create migration action type
70
     */
71 17
    protected function create($step)
72
    {
73 17
        $contentService = $this->repository->getContentService();
74 17
        $locationService = $this->repository->getLocationService();
75 17
        $contentTypeService = $this->repository->getContentTypeService();
76
77 17
        $contentTypeIdentifier = $step->dsl['content_type'];
78 17
        $contentTypeIdentifier = $this->resolveReference($contentTypeIdentifier);
79
        /// @todo use a contenttypematcher
80 17
        $contentType = $contentTypeService->loadContentTypeByIdentifier($contentTypeIdentifier);
81
82 17
        $contentCreateStruct = $contentService->newContentCreateStruct($contentType, $this->getLanguageCode($step));
83
84 17
        $this->setFields($contentCreateStruct, $step->dsl['attributes'], $contentType, $step);
85
86 16
        if (isset($step->dsl['always_available'])) {
87
            $contentCreateStruct->alwaysAvailable = $step->dsl['always_available'];
88
        } else {
89
            // Could be removed when https://github.com/ezsystems/ezpublish-kernel/pull/1874 is merged,
90
            // but we strive to support old eZ kernel versions as well...
91 16
            $contentCreateStruct->alwaysAvailable = $contentType->defaultAlwaysAvailable;
92
        }
93
94 16
        if (isset($step->dsl['remote_id'])) {
95 3
            $contentCreateStruct->remoteId = $step->dsl['remote_id'];
96
        }
97
98 16
        if (isset($step->dsl['section'])) {
99 1
            $sectionKey = $this->resolveReference($step->dsl['section']);
100 1
            $section = $this->sectionMatcher->matchOneByKey($sectionKey);
101 1
            $contentCreateStruct->sectionId = $section->id;
102
        }
103
104 16
        if (isset($step->dsl['owner'])) {
105 4
            $owner = $this->getUser($step->dsl['owner']);
106 4
            $contentCreateStruct->ownerId = $owner->id;
107
        }
108
109
        // This is a bit tricky, as the eZPublish API does not support having a different creator and owner with only 1 version.
110
        // We allow it, hoping that nothing gets broken because of it
111 16
        if (isset($step->dsl['version_creator'])) {
112 1
            $realContentOwnerId = $contentCreateStruct->ownerId;
113 1
            if ($realContentOwnerId == null) {
114 1
                $realContentOwnerId = $this->repository->getCurrentUser()->id;
115
            }
116 1
            $versionCreator = $this->getUser($step->dsl['version_creator']);
117 1
            $contentCreateStruct->ownerId = $versionCreator->id;
118
        }
119
120 16
        if (isset($step->dsl['modification_date'])) {
121 1
            $contentCreateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']);
122
        }
123
124
        // instantiate a location create struct from the parent location:
125
        // BC
126 16
        $locationId = isset($step->dsl['parent_location']) ? $step->dsl['parent_location'] : (
127 16
            isset($step->dsl['main_location']) ? $step->dsl['main_location'] : null
128
        );
129
        // 1st resolve references
130 16
        $locationId = $this->resolveReference($locationId);
131
        // 2nd allow to specify the location via remote_id
132 16
        $locationId = $this->locationManager->matchLocationByKey($locationId)->id;
133 16
        $locationCreateStruct = $locationService->newLocationCreateStruct($locationId);
134
135 16
        if (isset($step->dsl['location_remote_id'])) {
136 2
            $locationCreateStruct->remoteId = $step->dsl['location_remote_id'];
137
        }
138
139 16
        if (isset($step->dsl['priority'])) {
140 1
            $locationCreateStruct->priority = $step->dsl['priority'];
141
        }
142
143 16
        if (isset($step->dsl['is_hidden'])) {
144 1
            $locationCreateStruct->hidden = $step->dsl['is_hidden'];
145
        }
146
147 16
        if (isset($step->dsl['sort_field'])) {
148 1
            $locationCreateStruct->sortField = $this->sortConverter->hash2SortField($step->dsl['sort_field']);
149
        } else {
150 16
            $locationCreateStruct->sortField = $contentType->defaultSortField;
151
        }
152
153 16
        if (isset($step->dsl['sort_order'])) {
154 1
            $locationCreateStruct->sortOrder = $this->sortConverter->hash2SortOrder($step->dsl['sort_order']);
155
        } else {
156 16
            $locationCreateStruct->sortOrder = $contentType->defaultSortOrder;
157
        }
158
159 16
        $locations = array($locationCreateStruct);
160
161
        // BC
162 16
        $other_locations = isset($step->dsl['other_parent_locations']) ? $step->dsl['other_parent_locations'] : (
163 16
            isset($step->dsl['other_locations']) ? $step->dsl['other_locations'] : null
164
        );
165 16
        if (isset($other_locations)) {
166
            foreach ($other_locations as $locationId) {
167
                $locationId = $this->resolveReference($locationId);
168
                $locationId = $this->locationManager->matchLocationByKey($locationId)->id;
169
                $secondaryLocationCreateStruct = $locationService->newLocationCreateStruct($locationId);
170
                array_push($locations, $secondaryLocationCreateStruct);
171
            }
172
        }
173
174
        // create a draft using the content and location create struct and publish it
175 16
        $draft = $contentService->createContent($contentCreateStruct, $locations);
176 13
        $content = $contentService->publishVersion($draft->versionInfo);
177
178 13
        if (isset($step->dsl['object_states'])) {
179 2
            $this->setObjectStates($content, $step->dsl['object_states']);
180
        }
181
182
        // 2nd part of the hack: re-set the content owner to its intended value
183 13
        if (isset($step->dsl['version_creator']) || isset($step->dsl['publication_date'])) {
184 1
            $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
185
186 1
            if (isset($step->dsl['version_creator'])) {
187 1
                $contentMetaDataUpdateStruct->ownerId = $realContentOwnerId;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $realContentOwnerId does not seem to be defined for all execution paths leading up to this point.
Loading history...
188
            }
189 1
            if (isset($step->dsl['publication_date'])) {
190 1
                $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($step->dsl['publication_date']);
191
            }
192
            // we have to do this to make sure we preserve the custom modification date
193 1
            if (isset($step->dsl['modification_date'])) {
194 1
                $contentMetaDataUpdateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']);
195
            }
196
197 1
            $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct);
198
        }
199
200 13
        $this->setReferences($content, $step);
201
202 13
        return $content;
203
    }
204
205 13
    protected function load($step)
206
    {
207 13
        $contentCollection = $this->matchContents('load', $step);
208
209 13
        $this->validateResultsCount($contentCollection, $step);
210
211 7
        $this->setReferences($contentCollection, $step);
212
213 7
        return $contentCollection;
214
    }
215
216
    /**
217
     * Handles the content update migration action type
218
     *
219
     * @todo handle updating of more metadata fields
220
     */
221 10
    protected function update($step)
222
    {
223 10
        $contentService = $this->repository->getContentService();
224 10
        $contentTypeService = $this->repository->getContentTypeService();
225
226 10
        $contentCollection = $this->matchContents('update', $step);
227
228 10
        $this->validateResultsCount($contentCollection, $step);
229
230 10
        if (count($contentCollection) > 1 && isset($step->dsl['main_location'])) {
231
            throw new MigrationBundleException("Can not execute Content update because multiple contents match, and a main_location section is specified in the dsl.");
232
        }
233
234 10
        $contentType = array();
235
236 10
        foreach ($contentCollection as $key => $content) {
237 10
            $contentInfo = $content->contentInfo;
238
239 10
            if (!isset($contentType[$contentInfo->contentTypeId])) {
240 10
                $contentType[$contentInfo->contentTypeId] = $contentTypeService->loadContentType($contentInfo->contentTypeId);
241
            }
242
243 10
            $updated = false;
244
245 10
            if (isset($step->dsl['attributes']) || isset($step->dsl['version_creator'])) {
246 4
                $contentUpdateStruct = $contentService->newContentUpdateStruct();
247
248 4
                if (isset($step->dsl['attributes'])) {
249 4
                    $this->setFields($contentUpdateStruct, $step->dsl['attributes'], $contentType[$contentInfo->contentTypeId], $step);
250
                }
251
252 4
                $versionCreator = null;
253 4
                if (isset($step->dsl['version_creator'])) {
254 1
                    $versionCreator = $this->getUser($step->dsl['version_creator']);
255
                }
256
257 4
                $draft = $contentService->createContentDraft($contentInfo, null, $versionCreator);
258 4
                $contentService->updateContent($draft->versionInfo, $contentUpdateStruct);
259 4
                $content = $contentService->publishVersion($draft->versionInfo);
260
261 4
                $updated = true;
262
            }
263
264 10
            if (isset($step->dsl['always_available']) ||
265 10
                isset($step->dsl['new_remote_id']) ||
266 9
                isset($step->dsl['owner']) ||
267 8
                isset($step->dsl['modification_date']) ||
268 10
                isset($step->dsl['publication_date'])) {
269
270
                $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
271 3
272
                if (isset($step->dsl['always_available'])) {
273 3
                    $contentMetaDataUpdateStruct->alwaysAvailable = $step->dsl['always_available'];
274
                }
275
276
                if (isset($step->dsl['new_remote_id'])) {
277 3
                    $contentMetaDataUpdateStruct->remoteId = $step->dsl['new_remote_id'];
278 2
                }
279
280
                if (isset($step->dsl['owner'])) {
281 3
                    $owner = $this->getUser($step->dsl['owner']);
282 2
                    $contentMetaDataUpdateStruct->ownerId = $owner->id;
283 2
                }
284
285
                if (isset($step->dsl['modification_date'])) {
286 3
                    $contentMetaDataUpdateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']);
287 1
                }
288
289
                if (isset($step->dsl['publication_date'])) {
290 3
                    $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($step->dsl['publication_date']);
291 1
                }
292
293
                $content = $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct);
294 3
295
                $updated = true;
296 3
            }
297
298
            if (isset($step->dsl['section'])) {
299 10
                $this->setSection($content, $step->dsl['section']);
300 2
301
                $updated = true;
302 2
            }
303
304
            if (isset($step->dsl['object_states'])) {
305 10
                $this->setObjectStates($content, $step->dsl['object_states']);
306 1
307
                $updated = true;
308 1
            }
309
310
            if (isset($step->dsl['main_location'])) {
311 10
                $this->setMainLocation($content, $step->dsl['main_location']);
312 1
313
                $updated = true;
314 1
            }
315
316
            if (!$updated) {
317 10
                /// @todo check: should we not throw before attempting to make any changes, in case the migration is run without transactions?
318
                throw new InvalidStepDefinitionException("Can not execute Content update because there is nothing to update in the migration step as defined.");
319 1
            }
320
321
            $contentCollection[$key] = $content;
322 9
        }
323
324
        $this->setReferences($contentCollection, $step);
325 9
326
        return $contentCollection;
327 9
    }
328
329
    /**
330
     * Handles the content delete migration action type
331
     */
332
    protected function delete($step)
333 11
    {
334
        $contentCollection = $this->matchContents('delete', $step);
335 11
336
        $this->validateResultsCount($contentCollection, $step);
337 11
338
        $this->setReferences($contentCollection, $step);
339 11
340
        $contentService = $this->repository->getContentService();
341 11
342
        foreach ($contentCollection as $content) {
343 11
            try {
344
                $contentService->deleteContent($content->contentInfo);
345 11
            } catch (NotFoundException $e) {
346 2
                // Someone else (or even us, by virtue of location tree?) removed the content which we found just a
347
                // second ago. We can safely ignore this
348
            }
349
        }
350
351
        return $contentCollection;
352 11
    }
353
354
    /**
355
     * @param string $action
356
     * @return ContentCollection
357
     * @throws \Exception
358
     */
359
    protected function matchContents($action, $step)
360 22
    {
361
        if (!isset($step->dsl['object_id']) && !isset($step->dsl['remote_id']) && !isset($step->dsl['match'])) {
362 22
            throw new InvalidStepDefinitionException("The id or remote id of an object or a match condition is required to $action a content");
363
        }
364
365
        // Backwards compat
366
367
        if (isset($step->dsl['match'])) {
368 22
            $match = $step->dsl['match'];
369 22
        } else {
370
            if (isset($step->dsl['object_id'])) {
371 1
                $match = array('content_id' => $step->dsl['object_id']);
372 1
            } elseif (isset($step->dsl['remote_id'])) {
373
                $match = array('content_remote_id' => $step->dsl['remote_id']);
374
            }
375
        }
376
377
        // convert the references passed in the match
378
        $match = $this->resolveReferencesRecursively($match);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $match does not seem to be defined for all execution paths leading up to this point.
Loading history...
379 22
380
        $offset = isset($step->dsl['match_offset']) ? $this->resolveReference($step->dsl['match_offset']) : 0;
381 22
        $limit = isset($step->dsl['match_limit']) ? $this->resolveReference($step->dsl['match_limit']) : 0;
382 22
        $sort = isset($step->dsl['match_sort']) ? $this->resolveReference($step->dsl['match_sort']) : array();
383 22
384
        $tolerateMisses = isset($step->dsl['match_tolerate_misses']) ? $this->resolveReference($step->dsl['match_tolerate_misses']) : false;
385 22
386
        return $this->contentMatcher->match($match, $sort, $offset, $limit, $tolerateMisses);
387 22
    }
388
389
    /**
390
     * @param Content|VersionInfo $content
391
     * @param array $references the definitions of the references to set
392
     * @throws \InvalidArgumentException When trying to assign a reference to an unsupported attribute
393
     * @throws InvalidStepDefinitionException
394
     * @return array key: the reference names, values: the reference values
395
     */
396
    protected function getReferencesValues($content, array $references, $step)
397 13
    {
398
        $refs = array();
399 13
400
        foreach ($references as $key => $reference) {
401 13
402
            $reference = $this->parseReferenceDefinition($key, $reference);
403 12
404
            switch ($reference['attribute']) {
405 12
                case 'object_id':
406 12
                case 'content_id':
407 12
                case 'id':
408 12
                    $value = $content->id;
409 10
                    break;
410 10
                case 'remote_id':
411 9
                case 'content_remote_id':
412 8
                    $value = $content->contentInfo->remoteId;
413 4
                    break;
414 4
                case 'always_available':
415 7
                    $value = $content->contentInfo->alwaysAvailable;
416 1
                    break;
417 1
                case 'content_type_id':
418 7
                    $value = $content->contentInfo->contentTypeId;
419 1
                    break;
420 1
                case 'content_type_identifier':
421 7
                    $contentTypeService = $this->repository->getContentTypeService();
422 1
                    $value = $contentTypeService->loadContentType($content->contentInfo->contentTypeId)->identifier;
423 1
                    break;
424 1
                case 'current_version':
425 7
                case 'current_version_no':
426 7
                    $value = $content->contentInfo->currentVersionNo;
427 1
                    break;
428 1
                case 'location_id':
429 7
                case 'main_location_id':
430 6
                    $value = $content->contentInfo->mainLocationId;
431 3
                    break;
432 3
                case 'location_remote_id':
433 6
                    $locationService = $this->repository->getLocationService();
434 1
                    $value = $locationService->loadLocation($content->contentInfo->mainLocationId)->remoteId;
435 1
                    break;
436 1
                case 'main_language_code':
437 5
                    $value = $content->contentInfo->mainLanguageCode;
438 1
                    break;
439 1
                case 'modification_date':
440 5
                    $value = $content->contentInfo->modificationDate->getTimestamp();
441 1
                    break;
442 1
                case 'name':
443 5
                    $value = $content->contentInfo->name;
444 1
                    break;
445 1
                case 'owner_id':
446 5
                    $value = $content->contentInfo->ownerId;
447 1
                    break;
448 1
                case 'path':
449 5
                    $locationService = $this->repository->getLocationService();
450 2
                    $value = $locationService->loadLocation($content->contentInfo->mainLocationId)->pathString;
451 2
                    break;
452 2
                case 'publication_date':
453 4
                    $value = $content->contentInfo->publishedDate->getTimestamp();
454 1
                    break;
455 1
                case 'section_id':
456 4
                    $value = $content->contentInfo->sectionId;
457 1
                    break;
458 1
                case 'section_identifier':
459 4
                    $sectionService = $this->repository->getSectionService();
460 1
                    $value = $sectionService->loadSection($content->contentInfo->sectionId)->identifier;
461 1
                    break;
462 1
                case 'version_count':
463 4
                    $contentService = $this->repository->getContentService();
464 1
                    $value = count($contentService->loadVersions($content->contentInfo));
465 1
                    break;
466 1
                default:
467
                    if (strpos($reference['attribute'], 'object_state.') === 0) {
468 3
                        $stateGroupKey = substr($reference['attribute'], 13);
469 1
                        $stateGroup = $this->objectStateGroupMatcher->matchOneByKey($stateGroupKey);
470 1
                        $value = $stateGroupKey . '/' . $this->repository->getObjectStateService()->
471 1
                            getContentState($content->contentInfo, $stateGroup)->identifier;
472 1
                        break;
473 1
                    }
474
475
                    // allow to get the value of fields as well as their sub-parts
476
                    if (strpos($reference['attribute'], 'attributes.') === 0) {
477 2
                        $contentType = $this->repository->getContentTypeService()->loadContentType(
478 2
                            $content->contentInfo->contentTypeId
479 2
                        );
480
                        $parts = explode('.', $reference['attribute']);
481 2
                        // totally not sure if this list of special chars is correct for what could follow a jmespath identifier...
482
                        // also what about quoted strings?
483
                        $fieldIdentifier = preg_replace('/[[(|&!{].*$/', '', $parts[1]);
484 2
                        $field = $content->getField($fieldIdentifier);
0 ignored issues
show
Bug introduced by
The method getField() does not exist on eZ\Publish\API\Repository\Values\Content\Content. Did you maybe mean getFieldValue()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

484
                        /** @scrutinizer ignore-call */ 
485
                        $field = $content->getField($fieldIdentifier);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getField() does not exist on eZ\Publish\API\Repositor...ues\Content\VersionInfo. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

484
                        /** @scrutinizer ignore-call */ 
485
                        $field = $content->getField($fieldIdentifier);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
485 2
                        $fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier);
486 2
                        $hashValue = $this->fieldHandlerManager->fieldValueToHash(
487 2
                            $fieldDefinition->fieldTypeIdentifier, $contentType->identifier, $field->value
488 2
                        );
489
                        if (is_array($hashValue)) {
490 2
                            if (count($parts) == 2 && $fieldIdentifier === $parts[1]) {
491 2
                                $value = $hashValue;
492
                            } else {
493
                                $value = JmesPath::search(implode('.', array_slice($parts, 1)), array($fieldIdentifier => $hashValue));
494 2
                            }
495
                            // we do allow array values for refs, but not objects/resources. Q: will hashValue ever have objects in it?
496
                            if (!$this->isValidReferenceValue($value)) {
497 2
                                throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute'] . ': the given value has a non scalar/array element');
498 2
                            }
499
                        } else {
500
                            if (count($parts) > 2) {
501
                                /// @todo use a MigrationBundleException ?
502
                                throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute'] . ': the given attribute has a scalar value');
503
                            }
504
                            $value = $hashValue;
505
                        }
506
                        break;
507
                    }
508
509
                    throw new InvalidStepDefinitionException('Content Manager does not support setting references for attribute ' . $reference['attribute']);
510
            }
511
512 2
            $refs[$reference['identifier']] = $value;
513
        }
514
515
        return $refs;
516
    }
517
518 12
    /**
519
     * @param array $matchConditions
520
     * @param string $mode
521 13
     * @param array $context
522
     * @return array
523
     *
524
     * @throws \Exception
525
     * @todo add 2ndary locations when in 'update' mode
526
     * @todo add dumping of sort_field and sort_order for 2ndary locations
527
     * @todo allow context options to tweak the generated migrations eg:
528
     *       - omit rids on create
529
     *       - omit 2ndary locations on create
530
     *       - viceversa, do a full creation of 2ndary locations on create (incl. visibility, priority)
531
     *       - match by rid vs match by id on update and on delete
532
     *       - etc...
533
     */
534
    public function generateMigration(array $matchConditions, $mode, array $context = array())
535
    {
536
        $data = array();
537
        $previousUserId = $this->loginUser($this->getAdminUserIdentifierFromContext($context));
538
        try {
539
            $contentCollection = $this->contentMatcher->match($matchConditions);
540 5
            /// @todo throw if nothing is matched?
541
542 5
            /** @var \eZ\Publish\API\Repository\Values\Content\Content $content */
543 5
            foreach ($contentCollection as $content) {
544
545 5
                $location = $this->repository->getLocationService()->loadLocation($content->contentInfo->mainLocationId);
546
                $contentType = $this->repository->getContentTypeService()->loadContentType(
547
                    $content->contentInfo->contentTypeId
548 5
                );
549
550 5
                $contentData = array(
551 5
                    'type' => reset($this->supportedStepTypes),
552 5
                    'mode' => $mode
553
                );
554
555
                switch ($mode) {
556 5
                    case 'create':
557 5
                        $contentData = array_merge(
558
                            $contentData,
559
                            array(
560 5
                                'content_type' => $contentType->identifier,
561 5
                                'parent_location' => $location->parentLocationId,
562 3
                                'priority' => $location->priority,
563 3
                                'is_hidden' => $location->invisible,
564
                                'sort_field' => $this->sortConverter->sortField2Hash($location->sortField),
565 3
                                'sort_order' => $this->sortConverter->sortOrder2Hash($location->sortOrder),
566 3
                                'remote_id' => $content->contentInfo->remoteId,
567 3
                                'location_remote_id' => $location->remoteId,
568 3
                                'section' => $content->contentInfo->sectionId,
569 3
                                'object_states' => $this->getObjectStates($content),
570 3
                            )
571 3
                        );
572 3
                        $locationService = $this->repository->getLocationService();
573 3
                        /// @todo for accurate replication, we should express the adding of 2ndary locations as separate steps, and copy over visibility, priority etc
574 3
                        $locations = $locationService->loadLocations($content->contentInfo);
575
                        if (count($locations) > 1) {
576
                            $otherParentLocations = array();
577 3
                            foreach ($locations as $otherLocation) {
578
                                if ($otherLocation->id != $location->id) {
579 3
                                    $otherParentLocations[] = $otherLocation->parentLocationId;
580 3
                                }
581
                            }
582
                            $contentData['other_parent_locations'] = $otherParentLocations;
583
                        }
584
                        break;
585
                    case 'update':
586
                        $contentData = array_merge(
587
                            $contentData,
588
                            array(
589 3
                                'match' => array(
590 2
                                    ContentMatcher::MATCH_CONTENT_REMOTE_ID => $content->contentInfo->remoteId
591 1
                                ),
592 1
                                'new_remote_id' => $content->contentInfo->remoteId,
593
                                'section' => $content->contentInfo->sectionId,
594
                                'object_states' => $this->getObjectStates($content),
595 1
                            )
596
                        );
597 1
                        break;
598 1
                    case 'delete':
599 1
                        $contentData = array_merge(
600
                            $contentData,
601
                            array(
602 1
                                'match' => array(
603 1
                                    ContentMatcher::MATCH_CONTENT_REMOTE_ID => $content->contentInfo->remoteId
604 1
                                )
605 1
                            )
606
                        );
607
                        break;
608 1
                    default:
609
                        throw new InvalidStepDefinitionException("Executor 'content' doesn't support mode '$mode'");
610
                }
611
612 1
                if ($mode != 'delete') {
613
614
                    $language = $this->getLanguageCodeFromContext($context);
615
                    if ($language == 'all') {
616
                        $languages = $content->versionInfo->languageCodes;
617 5
                    } else {
618
                        $contentData = array_merge(
619 4
                            $contentData,
620 4
                            array(
621
                                'lang' => $language,
622
                            )
623 4
                        );
624 4
                        $languages = array($language);
625
                    }
626 4
627
                    $attributes = array();
628
                    foreach ($languages as $lang) {
629 4
                        foreach ($content->getFieldsByLanguage($lang) as $fieldIdentifier => $field) {
630
                            $fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier);
631
                            $fieldValue = $this->fieldHandlerManager->fieldValueToHash(
632 4
                                $fieldDefinition->fieldTypeIdentifier, $contentType->identifier, $field->value
633 4
                            );
634 4
                            if ($language == 'all') {
635 4
                                $attributes[$field->fieldDefIdentifier][$lang] = $fieldValue;
636 4
                            } else {
637 4
                                $attributes[$field->fieldDefIdentifier] = $fieldValue;
638
                            }
639 4
                        }
640
                    }
641
642 4
                    $contentData = array_merge(
643
                        $contentData,
644
                        array(
645
                            'section' => $content->contentInfo->sectionId,
646
                            'owner' => $content->contentInfo->ownerId,
647 4
                            'modification_date' => $content->contentInfo->modificationDate->getTimestamp(),
648 4
                            'publication_date' => $content->contentInfo->publishedDate->getTimestamp(),
649
                            'always_available' => (bool)$content->contentInfo->alwaysAvailable,
650 4
                            'attributes' => $attributes
651 4
                        )
652 4
                    );
653 4
                }
654 4
655 4
                $data[] = $contentData;
656
            }
657
        } finally {
658
            $this->loginUser($previousUserId);
659
        }
660 5
661
        return $data;
662
    }
663 5
664 5
    /**
665
     * @return string[]
666
     */
667
    public function listAllowedConditions()
668
    {
669
        return $this->contentMatcher->listAllowedConditions();
670
    }
671
672
    /**
673
     * Helper function to set the fields of a ContentCreateStruct based on the DSL attribute settings.
674
     *
675
     * @param ContentCreateStruct|ContentUpdateStruct $createOrUpdateStruct
676
     * @param array $fields see description of expected format in code below
677
     * @param ContentType $contentType
678
     * @param $step
679
     * @throws \Exception
680
     */
681
    protected function setFields($createOrUpdateStruct, array $fields, ContentType $contentType, $step)
682
    {
683
        $fields = $this->normalizeFieldDefs($fields, $step);
684 17
685
        foreach ($fields as $fieldIdentifier => $fieldLanguages) {
686 17
            foreach ($fieldLanguages as $language => $fieldValue) {
687
                if (!isset($contentType->fieldDefinitionsByIdentifier[$fieldIdentifier])) {
688 17
                    throw new MigrationBundleException("Field '$fieldIdentifier' is not present in content type '{$contentType->identifier}'");
689 17
                }
690 17
691 1
                $fieldDefinition = $contentType->fieldDefinitionsByIdentifier[$fieldIdentifier];
692
                $fieldValue = $this->getFieldValue($fieldValue, $fieldDefinition, $contentType->identifier, $step->context);
693
                $createOrUpdateStruct->setField($fieldIdentifier, $fieldValue, $language);
694 16
            }
695 16
        }
696 16
    }
697
698
    /**
699 16
     * Helper function to accommodate the definition of fields
700
     * - using a legacy DSL version
701
     * - using either single-language or multi-language style
702
     *
703
     * @param array $fields
704
     * @return array
705
     */
706
    protected function normalizeFieldDefs($fields, $step)
707
    {
708
        $convertedFields = [];
709 17
        $i = 0;
710
        // the 'easy' yml: key = field name, value = value
711 17
        // deprecated: the 'legacy' yml: key = numerical index, value = array ( field name => value )
712 17
        foreach ($fields as $key => $field) {
713
            if ($key === $i && is_array($field) && count($field) == 1) {
714
                // each $field is one key value pair
715 17
                // eg.: $field = array($fieldIdentifier => $fieldValue)
716 17
                reset($field);
717
                $fieldIdentifier = key($field);
718
                $fieldValue = $field[$fieldIdentifier];
719 10
720 10
                $convertedFields[$fieldIdentifier] = $fieldValue;
721 10
            } else {
722
                $convertedFields[$key] = $field;
723 10
            }
724
            $i++;
725 9
        }
726
727 17
        // transform single-language field defs in multilang ones
728
        if (!$this->hasLanguageCodesAsKeys($convertedFields)) {
729
            $language = $this->getLanguageCode($step);
730
731 17
            foreach ($convertedFields as $fieldIdentifier => $fieldValue) {
732 16
                $convertedFields[$fieldIdentifier] = array($language => $fieldValue);
733
            }
734 16
        }
735 16
736
        return $convertedFields;
737
    }
738
739 17
    /**
740
     * Checks whether all fields are using multilang syntax ie. a valid language as key.
741
     *
742
     * @param array $fields
743
     * @return bool
744
     */
745
    protected function hasLanguageCodesAsKeys(array $fields)
746
    {
747
        $languageCodes = $this->getContentLanguageCodes();
748 17
749
        foreach ($fields as $fieldIdentifier => $fieldData) {
750 17
            if (!is_array($fieldData) || empty($fieldData)) {
751
                return false;
752 17
            }
753 17
754 15
            foreach ($fieldData as $key => $data) {
755
                if (!in_array($key, $languageCodes, true)) {
756
                    return false;
757 6
                }
758 6
            }
759 3
        }
760
761
        return true;
762
    }
763
764 2
    /**
765
     * Returns all enabled Languages in the repo.
766
     * @todo move to parent class?
767
     *
768
     * @return string[]
769
     */
770
    protected function getContentLanguageCodes()
771
    {
772
        return array_map(
773 17
            function($language) {
774
                return $language->languageCode;
775 17
            },
776
            array_filter(
777 17
                $this->repository->getContentLanguageService()->loadLanguages(),
778 17
                function ($language) {
779 17
                    return $language->enabled;
780 17
                }
781
            )
782 17
        );
783 17
    }
784
785
    protected function setSection(Content $content, $sectionKey)
786
    {
787
        $sectionKey = $this->resolveReference($sectionKey);
788 2
        $section = $this->sectionMatcher->matchOneByKey($sectionKey);
789
790 2
        $sectionService = $this->repository->getSectionService();
791 2
        $sectionService->assignSection($content->contentInfo, $section);
792
    }
793 2
794 2
    protected function setObjectStates(Content $content, array $stateKeys)
795 2
    {
796
        foreach ($stateKeys as $stateKey) {
797 2
            $stateKey = $this->resolveReference($stateKey);
798
            /** @var \eZ\Publish\API\Repository\Values\ObjectState\ObjectState $state */
799 2
            $state = $this->objectStateMatcher->matchOneByKey($stateKey);
800 2
801
            $stateService = $this->repository->getObjectStateService();
802 2
            $stateService->setContentState($content->contentInfo, $state->getObjectStateGroup(), $state);
803
        }
804 2
    }
805 2
806
    protected function setMainLocation(Content $content, $locationId)
807 2
    {
808
        $locationId = $this->resolveReference($locationId);
809 1
        if (is_int($locationId) || ctype_digit($locationId)) {
810
            $location = $this->repository->getLocationService()->loadLocation($locationId);
811 1
        } else {
812 1
            $location = $this->repository->getLocationService()->loadLocationByRemoteId($locationId);
813 1
        }
814
815
        if ($location->contentInfo->id != $content->id) {
816
            throw new MigrationBundleException("Can not set main location {$location->id} to content {$content->id} as it belongs to another object");
817
        }
818 1
819
        $contentService = $this->repository->getContentService();
820
        $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
821
        $contentMetaDataUpdateStruct->mainLocationId = $location->id;
822 1
        $contentService->updateContentMetadata($location->contentInfo, $contentMetaDataUpdateStruct);
823 1
    }
824 1
825 1
    protected function getObjectStates(Content $content)
826 1
    {
827
        $states = [];
828 4
829
        $objectStateService = $this->repository->getObjectStateService();
830 4
        $groups = $objectStateService->loadObjectStateGroups();
831
        foreach ($groups as $group) {
832 4
            if (in_array($group->identifier, $this->ignoredStateGroupIdentifiers)) {
833 4
                continue;
834 4
            }
835 4
            $state = $objectStateService->getContentState($content->contentInfo, $group);
836 4
            $states[] = $group->identifier . '/' . $state->identifier;
837
        }
838
839
        return $states;
840
    }
841
842 4
    /**
843
     * Create the field value from the migration definition hash
844
     *
845
     * @param mixed $value
846
     * @param FieldDefinition $fieldDefinition
847
     * @param string $contentTypeIdentifier
848
     * @param array $context
849
     * @throws \InvalidArgumentException
850
     * @return mixed
851
     */
852
    protected function getFieldValue($value, FieldDefinition $fieldDefinition, $contentTypeIdentifier, array $context = array())
853
    {
854
        $fieldTypeIdentifier = $fieldDefinition->fieldTypeIdentifier;
855 16
856
        if (is_array($value) || $this->fieldHandlerManager->managesField($fieldTypeIdentifier, $contentTypeIdentifier)) {
857 16
            // since we now allow ref values to be arrays, let's attempt a 1st pass at resolving them here instead of every single fieldHandler...
858
            /// @todo we should find a way to signal to the field handler that references have been resolved already for this value,
859 16
            ///       to avoid multiple passes of reference resolution
860
            if (is_string($value) && $this->fieldHandlerManager->doPreResolveStringReferences($fieldTypeIdentifier, $contentTypeIdentifier)) {
861
                $value = $this->resolveReference($value);
862
            }
863 7
            // inject info about the current content type and field into the context
864 1
            // q: why not let the fieldHandlerManager do that ?
865
            $context['contentTypeIdentifier'] = $contentTypeIdentifier;
866
            $context['fieldIdentifier'] = $fieldDefinition->identifier;
867
            return $this->fieldHandlerManager->hashToFieldValue($fieldTypeIdentifier, $contentTypeIdentifier, $value, $context);
868 7
        }
869 7
870 7
        return $this->getSingleFieldValue($value, $fieldDefinition, $contentTypeIdentifier, $context);
871
    }
872
873 15
    /**
874
     * Create the field value for a primitive field from the migration definition hash
875
     *
876
     * @param mixed $value
877
     * @param FieldDefinition $fieldDefinition
878
     * @param string $contentTypeIdentifier
879
     * @param array $context
880
     * @throws \InvalidArgumentException
881
     * @return mixed
882
     */
883
    protected function getSingleFieldValue($value, FieldDefinition $fieldDefinition, $contentTypeIdentifier, array $context = array())
0 ignored issues
show
Unused Code introduced by
The parameter $contentTypeIdentifier is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

883
    protected function getSingleFieldValue($value, FieldDefinition $fieldDefinition, /** @scrutinizer ignore-unused */ $contentTypeIdentifier, array $context = array())

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $fieldDefinition is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

883
    protected function getSingleFieldValue($value, /** @scrutinizer ignore-unused */ FieldDefinition $fieldDefinition, $contentTypeIdentifier, array $context = array())

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $context is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

883
    protected function getSingleFieldValue($value, FieldDefinition $fieldDefinition, $contentTypeIdentifier, /** @scrutinizer ignore-unused */ array $context = array())

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
884
    {
885
        // booleans were handled here. They are now handled as complextypes
886 15
887
        // q: do we really want this to happen by default on all scalar field values?
888
        // Note: if you want this *not* to happen, register a complex field for your scalar field...
889
        $value = $this->resolveReference($value);
890
891
        return $value;
892 15
    }
893
894 15
    /**
895
     * Load user using either login, email, id - resolving eventual references
896
     * @param int|string $userKey
897
     * @return \eZ\Publish\API\Repository\Values\User\User
898
     */
899
    protected function getUser($userKey)
900
    {
901
        $userKey = $this->resolveReference($userKey);
902 4
        return $this->userMatcher->matchOneByKey($userKey);
903
    }
904 4
905 4
    /**
906
     * @param int|string $date if integer, we assume a timestamp
907
     * @return \DateTime
908
     */
909
    protected function toDateTime($date)
910
    {
911
        if (is_int($date)) {
912 1
            return new \DateTime("@" . $date);
913
        } else {
914 1
            return new \DateTime($date);
915
        }
916
    }
917
}
918