Passed
Push — main ( f754fa...2a1ce4 )
by Gaetano
09:34
created

ContentManager::generateMigration()   F

Complexity

Conditions 14
Paths 485

Size

Total Lines 131
Code Lines 89

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 67
CRAP Score 14.238

Importance

Changes 0
Metric Value
cc 14
eloc 89
nc 485
nop 3
dl 0
loc 131
ccs 67
cts 75
cp 0.8933
crap 14.238
rs 2.4922
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
658
            $this->loginUser($previousUserId);
659
        } catch (\Exception $e) {
660 5
            $this->loginUser($previousUserId);
661
            throw $e;
662
        }
663 5
664 5
        return $data;
665
    }
666
667
    /**
668
     * @return string[]
669
     */
670
    public function listAllowedConditions()
671
    {
672
        return $this->contentMatcher->listAllowedConditions();
673
    }
674
675
    /**
676
     * Helper function to set the fields of a ContentCreateStruct based on the DSL attribute settings.
677
     *
678
     * @param ContentCreateStruct|ContentUpdateStruct $createOrUpdateStruct
679
     * @param array $fields see description of expected format in code below
680
     * @param ContentType $contentType
681
     * @param $step
682
     * @throws \Exception
683
     */
684 17
    protected function setFields($createOrUpdateStruct, array $fields, ContentType $contentType, $step)
685
    {
686 17
        $fields = $this->normalizeFieldDefs($fields, $step);
687
688 17
        foreach ($fields as $fieldIdentifier => $fieldLanguages) {
689 17
            foreach ($fieldLanguages as $language => $fieldValue) {
690 17
                if (!isset($contentType->fieldDefinitionsByIdentifier[$fieldIdentifier])) {
691 1
                    throw new MigrationBundleException("Field '$fieldIdentifier' is not present in content type '{$contentType->identifier}'");
692
                }
693
694 16
                $fieldDefinition = $contentType->fieldDefinitionsByIdentifier[$fieldIdentifier];
695 16
                $fieldValue = $this->getFieldValue($fieldValue, $fieldDefinition, $contentType->identifier, $step->context);
696 16
                $createOrUpdateStruct->setField($fieldIdentifier, $fieldValue, $language);
697
            }
698
        }
699 16
    }
700
701
    /**
702
     * Helper function to accommodate the definition of fields
703
     * - using a legacy DSL version
704
     * - using either single-language or multi-language style
705
     *
706
     * @param array $fields
707
     * @return array
708
     */
709 17
    protected function normalizeFieldDefs($fields, $step)
710
    {
711 17
        $convertedFields = [];
712 17
        $i = 0;
713
        // the 'easy' yml: key = field name, value = value
714
        // deprecated: the 'legacy' yml: key = numerical index, value = array ( field name => value )
715 17
        foreach ($fields as $key => $field) {
716 17
            if ($key === $i && is_array($field) && count($field) == 1) {
717
                // each $field is one key value pair
718
                // eg.: $field = array($fieldIdentifier => $fieldValue)
719 10
                reset($field);
720 10
                $fieldIdentifier = key($field);
721 10
                $fieldValue = $field[$fieldIdentifier];
722
723 10
                $convertedFields[$fieldIdentifier] = $fieldValue;
724
            } else {
725 9
                $convertedFields[$key] = $field;
726
            }
727 17
            $i++;
728
        }
729
730
        // transform single-language field defs in multilang ones
731 17
        if (!$this->hasLanguageCodesAsKeys($convertedFields)) {
732 16
            $language = $this->getLanguageCode($step);
733
734 16
            foreach ($convertedFields as $fieldIdentifier => $fieldValue) {
735 16
                $convertedFields[$fieldIdentifier] = array($language => $fieldValue);
736
            }
737
        }
738
739 17
        return $convertedFields;
740
    }
741
742
    /**
743
     * Checks whether all fields are using multilang syntax ie. a valid language as key.
744
     *
745
     * @param array $fields
746
     * @return bool
747
     */
748 17
    protected function hasLanguageCodesAsKeys(array $fields)
749
    {
750 17
        $languageCodes = $this->getContentLanguageCodes();
751
752 17
        foreach ($fields as $fieldIdentifier => $fieldData) {
753 17
            if (!is_array($fieldData) || empty($fieldData)) {
754 15
                return false;
755
            }
756
757 6
            foreach ($fieldData as $key => $data) {
758 6
                if (!in_array($key, $languageCodes, true)) {
759 3
                    return false;
760
                }
761
            }
762
        }
763
764 2
        return true;
765
    }
766
767
    /**
768
     * Returns all enabled Languages in the repo.
769
     * @todo move to parent class?
770
     *
771
     * @return string[]
772
     */
773 17
    protected function getContentLanguageCodes()
774
    {
775 17
        return array_map(
776
            function($language) {
777 17
                return $language->languageCode;
778 17
            },
779 17
            array_filter(
780 17
                $this->repository->getContentLanguageService()->loadLanguages(),
781
                function ($language) {
782 17
                    return $language->enabled;
783 17
                }
784
            )
785
        );
786
    }
787
788 2
    protected function setSection(Content $content, $sectionKey)
789
    {
790 2
        $sectionKey = $this->resolveReference($sectionKey);
791 2
        $section = $this->sectionMatcher->matchOneByKey($sectionKey);
792
793 2
        $sectionService = $this->repository->getSectionService();
794 2
        $sectionService->assignSection($content->contentInfo, $section);
795 2
    }
796
797 2
    protected function setObjectStates(Content $content, array $stateKeys)
798
    {
799 2
        foreach ($stateKeys as $stateKey) {
800 2
            $stateKey = $this->resolveReference($stateKey);
801
            /** @var \eZ\Publish\API\Repository\Values\ObjectState\ObjectState $state */
802 2
            $state = $this->objectStateMatcher->matchOneByKey($stateKey);
803
804 2
            $stateService = $this->repository->getObjectStateService();
805 2
            $stateService->setContentState($content->contentInfo, $state->getObjectStateGroup(), $state);
806
        }
807 2
    }
808
809 1
    protected function setMainLocation(Content $content, $locationId)
810
    {
811 1
        $locationId = $this->resolveReference($locationId);
812 1
        if (is_int($locationId) || ctype_digit($locationId)) {
813 1
            $location = $this->repository->getLocationService()->loadLocation($locationId);
814
        } else {
815
            $location = $this->repository->getLocationService()->loadLocationByRemoteId($locationId);
816
        }
817
818 1
        if ($location->contentInfo->id != $content->id) {
819
            throw new MigrationBundleException("Can not set main location {$location->id} to content {$content->id} as it belongs to another object");
820
        }
821
822 1
        $contentService = $this->repository->getContentService();
823 1
        $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
824 1
        $contentMetaDataUpdateStruct->mainLocationId = $location->id;
825 1
        $contentService->updateContentMetadata($location->contentInfo, $contentMetaDataUpdateStruct);
826 1
    }
827
828 4
    protected function getObjectStates(Content $content)
829
    {
830 4
        $states = [];
831
832 4
        $objectStateService = $this->repository->getObjectStateService();
833 4
        $groups = $objectStateService->loadObjectStateGroups();
834 4
        foreach ($groups as $group) {
835 4
            if (in_array($group->identifier, $this->ignoredStateGroupIdentifiers)) {
836 4
                continue;
837
            }
838
            $state = $objectStateService->getContentState($content->contentInfo, $group);
839
            $states[] = $group->identifier . '/' . $state->identifier;
840
        }
841
842 4
        return $states;
843
    }
844
845
    /**
846
     * Create the field value from the migration definition hash
847
     *
848
     * @param mixed $value
849
     * @param FieldDefinition $fieldDefinition
850
     * @param string $contentTypeIdentifier
851
     * @param array $context
852
     * @throws \InvalidArgumentException
853
     * @return mixed
854
     */
855 16
    protected function getFieldValue($value, FieldDefinition $fieldDefinition, $contentTypeIdentifier, array $context = array())
856
    {
857 16
        $fieldTypeIdentifier = $fieldDefinition->fieldTypeIdentifier;
858
859 16
        if (is_array($value) || $this->fieldHandlerManager->managesField($fieldTypeIdentifier, $contentTypeIdentifier)) {
860
            // since we now allow ref values to be arrays, let's attempt a 1st pass at resolving them here instead of every single fieldHandler...
861
            /// @todo we should find a way to signal to the field handler that references have been resolved already for this value,
862
            ///       to avoid multiple passes of reference resolution
863 7
            if (is_string($value) && $this->fieldHandlerManager->doPreResolveStringReferences($fieldTypeIdentifier, $contentTypeIdentifier)) {
864 1
                $value = $this->resolveReference($value);
865
            }
866
            // inject info about the current content type and field into the context
867
            // q: why not let the fieldHandlerManager do that ?
868 7
            $context['contentTypeIdentifier'] = $contentTypeIdentifier;
869 7
            $context['fieldIdentifier'] = $fieldDefinition->identifier;
870 7
            return $this->fieldHandlerManager->hashToFieldValue($fieldTypeIdentifier, $contentTypeIdentifier, $value, $context);
871
        }
872
873 15
        return $this->getSingleFieldValue($value, $fieldDefinition, $contentTypeIdentifier, $context);
874
    }
875
876
    /**
877
     * Create the field value for a primitive field from the migration definition hash
878
     *
879
     * @param mixed $value
880
     * @param FieldDefinition $fieldDefinition
881
     * @param string $contentTypeIdentifier
882
     * @param array $context
883
     * @throws \InvalidArgumentException
884
     * @return mixed
885
     */
886 15
    protected function getSingleFieldValue($value, FieldDefinition $fieldDefinition, $contentTypeIdentifier, array $context = array())
0 ignored issues
show
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

886
    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 $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

886
    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 $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

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