Completed
Branch master (e35419)
by Gaetano
06:40
created

ContentManager::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 8
dl 0
loc 18
ccs 9
cts 9
cp 1
crap 1
rs 10
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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

453
                        /** @scrutinizer ignore-call */ 
454
                        $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...
454 1
                        $fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier);
455 1
                        $hashValue = $this->fieldHandlerManager->fieldValueToHash(
456 1
                            $fieldDefinition->fieldTypeIdentifier, $contentType->identifier, $field->value
457
                        );
458 1
                        if (is_array($hashValue)) {
459 1
                            if (count($parts) == 2 && $fieldIdentifier === $parts[1]) {
460
                                throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute'] . ': the given attribute has an array value');
461
                            }
462 1
                            $value = JmesPath::search(implode('.', array_slice($parts, 1)), array($fieldIdentifier => $hashValue));
463
                        } else {
464
                            if (count($parts) > 2) {
465
                                throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute'] . ': the given attribute has a scalar value');
466
                            }
467
                            $value = $hashValue;
468
                        }
469 1
                        break;
470
                    }
471
472
                    throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute']);
473
            }
474
475 9
            $refs[$reference['identifier']] = $value;
476
        }
477
478 9
        return $refs;
479
    }
480
481
    /**
482
     * @param array $matchCondition
483
     * @param string $mode
484
     * @param array $context
485
     * @throws \Exception
486
     * @return array
487
     *
488
     * @todo add support for dumping all object languages
489
     * @todo add 2ndary locations when in 'update' mode
490
     * @todo add dumping of sort_field and sort_order for 2ndary locations
491
     */
492 3
    public function generateMigration(array $matchCondition, $mode, array $context = array())
493
    {
494 3
        $previousUserId = $this->loginUser($this->getAdminUserIdentifierFromContext($context));
495 3
        $contentCollection = $this->contentMatcher->match($matchCondition);
496 3
        $data = array();
497
498
        /** @var \eZ\Publish\API\Repository\Values\Content\Content $content */
499 3
        foreach ($contentCollection as $content) {
500
501 3
            $location = $this->repository->getLocationService()->loadLocation($content->contentInfo->mainLocationId);
502 3
            $contentType = $this->repository->getContentTypeService()->loadContentType(
503 3
                $content->contentInfo->contentTypeId
504
            );
505
506
            $contentData = array(
507 3
                'type' => reset($this->supportedStepTypes),
508 3
                'mode' => $mode
509
            );
510
511
            switch ($mode) {
512 3
                case 'create':
513 1
                    $contentData = array_merge(
514 1
                        $contentData,
515
                        array(
516 1
                            'content_type' => $contentType->identifier,
517 1
                            'parent_location' => $location->parentLocationId,
518 1
                            'priority' => $location->priority,
519 1
                            'is_hidden' => $location->invisible,
520 1
                            'sort_field' => $this->sortConverter->sortField2Hash($location->sortField),
521 1
                            'sort_order' => $this->sortConverter->sortOrder2Hash($location->sortOrder),
522 1
                            'remote_id' => $content->contentInfo->remoteId,
523 1
                            'location_remote_id' => $location->remoteId,
524 1
                            'section' => $content->contentInfo->sectionId,
525 1
                            'object_states' => $this->getObjectStates($content),
526
                        )
527
                    );
528 1
                    $locationService = $this->repository->getLocationService();
529
                    /// @todo for accurate replication, we should express the addinfg of 2ndary locatins as separate steps, and copy over visibility, priority etc
530 1
                    $locations = $locationService->loadLocations($content->contentInfo);
531 1
                    if (count($locations) > 1) {
532
                        $otherParentLocations = array();
533
                        foreach($locations as $otherLocation) {
534
                            if ($otherLocation->id != $location->id) {
535
                                $otherParentLocations[] = $otherLocation->parentLocationId;
536
                            }
537
                        }
538
                        $contentData['other_parent_locations'] = $otherParentLocations;
539
                    }
540 1
                    break;
541 2
                case 'update':
542 1
                    $contentData = array_merge(
543 1
                        $contentData,
544
                        array(
545
                            'match' => array(
546 1
                                ContentMatcher::MATCH_CONTENT_REMOTE_ID => $content->contentInfo->remoteId
547
                            ),
548 1
                            'new_remote_id' => $content->contentInfo->remoteId,
549 1
                            'section' => $content->contentInfo->sectionId,
550 1
                            'object_states' => $this->getObjectStates($content),
551
                        )
552
                    );
553 1
                    break;
554 1
                case 'delete':
555 1
                    $contentData = array_merge(
556 1
                        $contentData,
557
                        array(
558
                            'match' => array(
559 1
                                ContentMatcher::MATCH_CONTENT_REMOTE_ID => $content->contentInfo->remoteId
560
                            )
561
                        )
562
                    );
563 1
                    break;
564
                default:
565
                    throw new \Exception("Executor 'content' doesn't support mode '$mode'");
566
            }
567
568 3
            if ($mode != 'delete') {
569
570 2
                $attributes = array();
571 2
                foreach ($content->getFieldsByLanguage($this->getLanguageCodeFromContext($context)) as $fieldIdentifier => $field) {
572 2
                    $fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier);
573 2
                    $attributes[$field->fieldDefIdentifier] = $this->fieldHandlerManager->fieldValueToHash(
574 2
                        $fieldDefinition->fieldTypeIdentifier, $contentType->identifier, $field->value
575
                    );
576
                }
577
578 2
                $contentData = array_merge(
579 2
                    $contentData,
580
                    array(
581 2
                        'lang' => $this->getLanguageCodeFromContext($context),
582 2
                        'section' => $content->contentInfo->sectionId,
583 2
                        'owner' => $content->contentInfo->ownerId,
584 2
                        'modification_date' => $content->contentInfo->modificationDate->getTimestamp(),
585 2
                        'publication_date' => $content->contentInfo->publishedDate->getTimestamp(),
586 2
                        'always_available' => (bool)$content->contentInfo->alwaysAvailable,
587 2
                        'attributes' => $attributes
588
                    )
589
                );
590
            }
591
592 3
            $data[] = $contentData;
593
        }
594
595 3
        $this->loginUser($previousUserId);
596 3
        return $data;
597
    }
598
599
    /**
600
     * Helper function to set the fields of a ContentCreateStruct based on the DSL attribute settings.
601
     *
602
     * @param ContentCreateStruct|ContentUpdateStruct $createOrUpdateStruct
603
     * @param array $fields see description of expected format in code below
604
     * @param ContentType $contentType
605
     * @param $step
606
     * @throws \Exception
607
     */
608 14
    protected function setFields($createOrUpdateStruct, array $fields, ContentType $contentType, $step)
609
    {
610 14
        $fields = $this->normalizeFieldDefs($fields, $step);
611
612 14
        foreach ($fields as $fieldIdentifier => $fieldLanguages) {
613 14
            foreach ($fieldLanguages as $language => $fieldValue) {
614 14
                if (!isset($contentType->fieldDefinitionsByIdentifier[$fieldIdentifier])) {
615 2
                    throw new \Exception("Field '$fieldIdentifier' is not present in content type '{$contentType->identifier}'");
616
                }
617
618 14
                $fieldDefinition = $contentType->fieldDefinitionsByIdentifier[$fieldIdentifier];
619 14
                $fieldValue = $this->getFieldValue($fieldValue, $fieldDefinition, $contentType->identifier, $step->context);
620 14
                $createOrUpdateStruct->setField($fieldIdentifier, $fieldValue, $language);
621
            }
622
        }
623 12
    }
624
625
    /**
626
     * Helper function to accommodate the definition of fields
627
     * - using a legacy DSL version
628
     * - using either single-language or multi-language style
629
     * 
630
     * @param array $fields
631
     * @return array
632
     */
633 14
    protected function normalizeFieldDefs($fields, $step)
634
    {
635 14
        $convertedFields = [];
636 14
        $i = 0;
637
        // the 'easy' yml: key = field name, value = value
638
        // deprecated: the 'legacy' yml: key = numerical index, value = array ( field name => value )
639 14
        foreach ($fields as $key => $field) {
640 14
            if ($key === $i && is_array($field) && count($field) == 1) {
641
                // each $field is one key value pair
642
                // eg.: $field = array($fieldIdentifier => $fieldValue)
643 9
                reset($field);
644 9
                $fieldIdentifier = key($field);
645 9
                $fieldValue = $field[$fieldIdentifier];
646
647 9
                $convertedFields[$fieldIdentifier] = $fieldValue;
648
            } else {
649 7
                $convertedFields[$key] = $field;
650
            }
651 14
            $i++;
652
        }
653
654
        // transform single-language field defs in multilang ones
655 14
        if (!$this->hasLanguageCodesAsKeys($convertedFields)) {
656 13
            $language = $this->getLanguageCode($step);
657
658 13
            foreach ($convertedFields as $fieldIdentifier => $fieldValue) {
659 13
                $convertedFields[$fieldIdentifier] = array($language => $fieldValue);
660
            }
661
        }
662
663 14
        return $convertedFields;
664
    }
665
666
    /**
667
     * Checks whether all fields are using multilang syntax ie. a valid language as key.
668
     *
669
     * @param array $fields
670
     * @return bool
671
     */
672 14
    protected function hasLanguageCodesAsKeys(array $fields)
673
    {
674 14
        $languageCodes = $this->getContentLanguageCodes();
675
676 14
        foreach ($fields as $fieldIdentifier => $fieldData) {
677 14
            if (!is_array($fieldData) || empty($fieldData)) {
678 12
                return false;
679
            }
680
681 5
            foreach ($fieldData as $key => $data) {
682 5
                if (!in_array($key, $languageCodes, true)) {
683 5
                    return false;
684
                }
685
            }
686
        }
687
688 2
        return true;
689
    }
690
691
    /**
692
     * Returns all enabled Languages in the repo.
693
     * @todo move to parent class?
694
     *
695
     * @return string[]
696
     */
697 14
    protected function getContentLanguageCodes()
698
    {
699 14
        return array_map(
700 14
            function($language) {
701 14
                return $language->languageCode;
702 14
            },
703 14
            array_filter(
704 14
                $this->repository->getContentLanguageService()->loadLanguages(),
705 14
                function ($language) {
706 14
                    return $language->enabled;
707 14
                }
708
            )
709
        );
710
    }
711
712 1
    protected function setSection(Content $content, $sectionKey)
713
    {
714 1
        $sectionKey = $this->referenceResolver->resolveReference($sectionKey);
715 1
        $section = $this->sectionMatcher->matchOneByKey($sectionKey);
716
717 1
        $sectionService = $this->repository->getSectionService();
718 1
        $sectionService->assignSection($content->contentInfo, $section);
719 1
    }
720
721 2
    protected function setObjectStates(Content $content, array $stateKeys)
722
    {
723 2
        foreach ($stateKeys as $stateKey) {
724 2
            $stateKey = $this->referenceResolver->resolveReference($stateKey);
725
            /** @var \eZ\Publish\API\Repository\Values\ObjectState\ObjectState $state */
726 2
            $state = $this->objectStateMatcher->matchOneByKey($stateKey);
727
728 2
            $stateService = $this->repository->getObjectStateService();
729 2
            $stateService->setContentState($content->contentInfo, $state->getObjectStateGroup(), $state);
730
        }
731 2
    }
732
733 1
    protected function setMainLocation(Content $content, $locationId)
734
    {
735 1
        $locationId = $this->referenceResolver->resolveReference($locationId);
736 1
        if (is_int($locationId) || ctype_digit($locationId)) {
737 1
            $location = $this->repository->getLocationService()->loadLocation($locationId);
738
        } else {
739
            $location = $this->repository->getLocationService()->loadLocationByRemoteId($locationId);
740
        }
741
742 1
        if ($location->contentInfo->id != $content->id) {
743
            throw new \Exception("Can not set main location {$location->id} to content {$content->id} as it belongs to another object");
744
        }
745
746 1
        $contentService = $this->repository->getContentService();
747 1
        $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
748 1
        $contentMetaDataUpdateStruct->mainLocationId = $location->id;
749 1
        $contentService->updateContentMetadata($location->contentInfo, $contentMetaDataUpdateStruct);
750 1
    }
751
752 2
    protected function getObjectStates(Content $content)
753
    {
754 2
        $states = [];
755
756 2
        $objectStateService = $this->repository->getObjectStateService();
757 2
        $groups = $objectStateService->loadObjectStateGroups();
758 2
        foreach ($groups as $group) {
759 2
            if (in_array($group->identifier, $this->ignoredStateGroupIdentifiers)) {
760 2
                continue;
761
            }
762
            $state = $objectStateService->getContentState($content->contentInfo, $group);
763
            $states[] = $group->identifier . '/' . $state->identifier;
764
        }
765
766 2
        return $states;
767
    }
768
769
    /**
770
     * Create the field value from the migration definition hash
771
     *
772
     * @param mixed $value
773
     * @param FieldDefinition $fieldDefinition
774
     * @param string $contentTypeIdentifier
775
     * @param array $context
776
     * @throws \InvalidArgumentException
777
     * @return mixed
778
     */
779 14
    protected function getFieldValue($value, FieldDefinition $fieldDefinition, $contentTypeIdentifier, array $context = array())
780
    {
781 14
        $fieldTypeIdentifier = $fieldDefinition->fieldTypeIdentifier;
782
783 14
        if (is_array($value) || $this->fieldHandlerManager->managesField($fieldTypeIdentifier, $contentTypeIdentifier)) {
784
            // since we now allow refs to be arrays, let's attempt a 1st pass at resolving them here instead of every single fieldHandler...
785 5
            if (is_string($value) && $this->fieldHandlerManager->doPreResolveStringReferences($fieldTypeIdentifier, $contentTypeIdentifier)) {
786 1
                $value = $this->referenceResolver->resolveReference($value);
787
            }
788
            // inject info about the current content type and field into the context
789 5
            $context['contentTypeIdentifier'] = $contentTypeIdentifier;
790 5
            $context['fieldIdentifier'] = $fieldDefinition->identifier;
791 5
            return $this->fieldHandlerManager->hashToFieldValue($fieldTypeIdentifier, $contentTypeIdentifier, $value, $context);
792
        }
793
794 12
        return $this->getSingleFieldValue($value, $fieldDefinition, $contentTypeIdentifier, $context);
795
    }
796
797
    /**
798
     * Create the field value for a primitive field from the migration definition hash
799
     *
800
     * @param mixed $value
801
     * @param FieldDefinition $fieldDefinition
802
     * @param string $contentTypeIdentifier
803
     * @param array $context
804
     * @throws \InvalidArgumentException
805
     * @return mixed
806
     */
807 12
    protected function getSingleFieldValue($value, FieldDefinition $fieldDefinition, $contentTypeIdentifier, array $context = array())
0 ignored issues
show
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

807
    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...
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

807
    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

807
    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...
808
    {
809
        // booleans were handled here. They are now handled as complextypes
810
811
        // q: do we really want this to happen by default on all scalar field values?
812
        // Note: if you want this *not* to happen, register a complex field for your scalar field...
813 12
        $value = $this->referenceResolver->resolveReference($value);
814
815 12
        return $value;
816
    }
817
818
    /**
819
     * Load user using either login, email, id - resolving eventual references
820
     * @param int|string $userKey
821
     * @return \eZ\Publish\API\Repository\Values\User\User
822
     */
823 2
    protected function getUser($userKey)
824
    {
825 2
        $userKey = $this->referenceResolver->resolveReference($userKey);
826 2
        return $this->userMatcher->matchOneByKey($userKey);
827
    }
828
829
    /**
830
     * @param int|string $date if integer, we assume a timestamp
831
     * @return \DateTime
832
     */
833 1
    protected function toDateTime($date)
834
    {
835 1
        if (is_int($date)) {
836
            return new \DateTime("@" . $date);
837
        } else {
838 1
            return new \DateTime($date);
839
        }
840
    }
841
}
842