Completed
Push — master ( 751bcb...cbbd52 )
by Gaetano
08:11
created

ContentManager::setReferences()   D

Complexity

Conditions 28
Paths 52

Size

Total Lines 100
Code Lines 80

Duplication

Lines 13
Ratio 13 %

Code Coverage

Tests 0
CRAP Score 812

Importance

Changes 0
Metric Value
dl 13
loc 100
ccs 0
cts 0
cp 0
rs 4.4803
c 0
b 0
f 0
cc 28
eloc 80
nc 52
nop 1
crap 812

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\ContentType\ContentType;
6
use eZ\Publish\API\Repository\Values\ContentType\FieldDefinition;
7
use eZ\Publish\API\Repository\Values\Content\Content;
8
use eZ\Publish\API\Repository\Values\Content\ContentCreateStruct;
9
use eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct;
10
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
11
use Kaliop\eZMigrationBundle\API\Collection\ContentCollection;
12
use Kaliop\eZMigrationBundle\API\MigrationGeneratorInterface;
13
use Kaliop\eZMigrationBundle\Core\ComplexField\ComplexFieldManager;
14
use Kaliop\eZMigrationBundle\Core\Matcher\ContentMatcher;
15
use Kaliop\eZMigrationBundle\Core\Matcher\SectionMatcher;
16
use Kaliop\eZMigrationBundle\Core\Matcher\UserMatcher;
17
use Kaliop\eZMigrationBundle\Core\Matcher\ObjectStateMatcher;
18
use Kaliop\eZMigrationBundle\Core\Helper\SortConverter;
19
use JmesPath\Env as JmesPath;
20
21
/**
22
 * Handles content migrations.
23
 *
24
 * @todo add support for updating of content metadata
25
 */
26
class ContentManager extends RepositoryExecutor implements MigrationGeneratorInterface
27 20
{
28
    protected $supportedStepTypes = array('content');
29 20
    protected $supportedActions = array('create', 'load', 'update', 'delete');
30 20
31 20
    protected $contentMatcher;
32
    protected $sectionMatcher;
33
    protected $userMatcher;
34
    protected $objectStateMatcher;
35
    protected $complexFieldManager;
36 4
    protected $locationManager;
37
    protected $sortConverter;
38 4
39 4
    public function __construct(
40 4
        ContentMatcher $contentMatcher,
41
        SectionMatcher $sectionMatcher,
42 4
        UserMatcher $userMatcher,
43 4
        ObjectStateMatcher $objectStateMatcher,
44 3
        ComplexFieldManager $complexFieldManager,
45 3
        LocationManager $locationManager,
46 4
        SortConverter $sortConverter
47
    ) {
48 4
        $this->contentMatcher = $contentMatcher;
49 4
        $this->sectionMatcher = $sectionMatcher;
50
        $this->userMatcher = $userMatcher;
51 4
        $this->objectStateMatcher = $objectStateMatcher;
52 1
        $this->complexFieldManager = $complexFieldManager;
53 1
        $this->locationManager = $locationManager;
54
        $this->sortConverter = $sortConverter;
55
    }
56 4
57 4
    /**
58 1
     * Handles the content create migration action type
59 1
     */
60 4
    protected function create()
61 4
    {
62 1
        $contentService = $this->repository->getContentService();
63 1
        $locationService = $this->repository->getLocationService();
64
        $contentTypeService = $this->repository->getContentTypeService();
65 4
66 1
        $contentTypeIdentifier = $this->dsl['content_type'];
67 1
        $contentTypeIdentifier = $this->referenceResolver->resolveReference($contentTypeIdentifier);
68
        /// @todo use a contenttypematcher
69 4
        $contentType = $contentTypeService->loadContentTypeByIdentifier($contentTypeIdentifier);
70
71 4
        $contentCreateStruct = $contentService->newContentCreateStruct($contentType, $this->getLanguageCode());
72
73
        $this->setFields($contentCreateStruct, $this->dsl['attributes'], $contentType);
74
75
        if (isset($this->dsl['always_available'])) {
76
            $contentCreateStruct->alwaysAvailable = $this->dsl['always_available'];
77
        } else {
78
            // Could be removed when https://github.com/ezsystems/ezpublish-kernel/pull/1874 is merged,
79
            // but we strive to support old eZ kernel versions as well...
80
            $contentCreateStruct->alwaysAvailable = $contentType->defaultAlwaysAvailable;
81
        }
82
83 4
        if (isset($this->dsl['remote_id'])) {
84 3
            $contentCreateStruct->remoteId = $this->dsl['remote_id'];
85
        }
86 3
87 3
        if (isset($this->dsl['section'])) {
88
            $sectionKey = $this->referenceResolver->resolveReference($this->dsl['section']);
89
            $section = $this->sectionMatcher->matchOneByKey($sectionKey);
90
            $contentCreateStruct->sectionId = $section->id;
91
        }
92
93 View Code Duplication
        if (isset($this->dsl['owner'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
94 1
            $owner = $this->getUser($this->dsl['owner']);
95
            $contentCreateStruct->ownerId = $owner->id;
96 1
        }
97 1
98
        // This is a bit tricky, as the eZPublish API does not support having a different creator and owner with only 1 version.
99
        // We allow it, hoping that nothing gets broken because of it
100
        if (isset($this->dsl['version_creator'])) {
101
            $realContentOwnerId = $contentCreateStruct->ownerId;
102
            if ($realContentOwnerId == null) {
103
                $realContentOwnerId = $this->repository->getCurrentUser()->id;
104
            }
105
            $versionCreator = $this->getUser($this->dsl['version_creator']);
106
            $contentCreateStruct->ownerId = $versionCreator->id;
107
        }
108
109
        if (isset($this->dsl['modification_date'])) {
110
            $contentCreateStruct->modificationDate = $this->toDateTime($this->dsl['modification_date']);
111
        }
112
113
        // instantiate a location create struct from the parent location:
114
        // BC
115
        $locationId = isset($this->dsl['parent_location']) ? $this->dsl['parent_location'] : (
116
            isset($this->dsl['main_location']) ? $this->dsl['main_location'] : null
117
        );
118
        // 1st resolve references
119
        $locationId = $this->referenceResolver->resolveReference($locationId);
120
        // 2nd allow to specify the location via remote_id
121 1
        $locationId = $this->locationManager->matchLocationByKey($locationId)->id;
122
        $locationCreateStruct = $locationService->newLocationCreateStruct($locationId);
123 1
124
        if (isset($this->dsl['location_remote_id'])) {
125
            $locationCreateStruct->remoteId = $this->dsl['location_remote_id'];
126
        }
127 1
128
        if (isset($this->dsl['priority'])) {
129 1
            $locationCreateStruct->priority = $this->dsl['priority'];
130 1
        }
131
132 1
        if (isset($this->dsl['is_hidden'])) {
133 1
            $locationCreateStruct->hidden = $this->dsl['is_hidden'];
134 1
        }
135
136 1
        if (isset($this->dsl['sort_field'])) {
137
            $locationCreateStruct->sortField = $this->sortConverter->hash2SortField($this->dsl['sort_field']);
138 1
        } else {
139 1
            $locationCreateStruct->sortField = $contentType->defaultSortField;
140 1
        }
141
142 1
        if (isset($this->dsl['sort_order'])) {
143 1
            $locationCreateStruct->sortOrder = $this->sortConverter->hash2SortOrder($this->dsl['sort_order']);
144 1
        } else {
145
            $locationCreateStruct->sortOrder = $contentType->defaultSortOrder;
146 1
        }
147
148
        $locations = array($locationCreateStruct);
149
150
        // BC
151
        $other_locations = isset($this->dsl['other_parent_locations']) ? $this->dsl['other_parent_locations'] : (
152
            isset($this->dsl['other_locations']) ? $this->dsl['other_locations'] : null
153
        );
154
        if (isset($other_locations)) {
155
            foreach ($other_locations as $locationId) {
156
                $locationId = $this->referenceResolver->resolveReference($locationId);
157
                $locationId = $this->locationManager->matchLocationByKey($locationId)->id;
158
                $secondaryLocationCreateStruct = $locationService->newLocationCreateStruct($locationId);
159
                array_push($locations, $secondaryLocationCreateStruct);
160
            }
161 1
        }
162 1
163 1
        // create a draft using the content and location create struct and publish it
164
        $draft = $contentService->createContent($contentCreateStruct, $locations);
165
        $content = $contentService->publishVersion($draft->versionInfo);
166
167
        if (isset($this->dsl['object_states'])) {
168 3
            $this->setObjectStates($content, $this->dsl['object_states']);
169
        }
170 3
171
        // 2nd part of the hack: re-set the content owner to its intended value
172 3
        if (isset($this->dsl['version_creator']) || isset($this->dsl['publication_date'])) {
173
            $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
174 3
175
            if (isset($this->dsl['version_creator'])) {
176 3
                $contentMetaDataUpdateStruct->ownerId = $realContentOwnerId;
0 ignored issues
show
Bug introduced by
The variable $realContentOwnerId does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
177 3
            }
178
            if (isset($this->dsl['publication_date'])) {
179
                $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($this->dsl['publication_date']);
180
            }
181 3
182 3
            $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct);
183
        }
184
185
        $this->setReferences($content);
186
187
        return $content;
188
    }
189 3
190 View Code Duplication
    protected function load()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
191 3
    {
192
        $contentCollection = $this->matchContents('load');
193
194
        if (count($contentCollection) > 1 && isset($this->dsl['references'])) {
195
            throw new \Exception("Can not execute Content load because multiple contents match, and a references section is specified in the dsl. References can be set when only 1 content matches");
196 3
        }
197 1
198 1
        $this->setReferences($contentCollection);
0 ignored issues
show
Bug introduced by
It seems like $contentCollection defined by $this->matchContents('load') on line 192 can be null; however, Kaliop\eZMigrationBundle...anager::setReferences() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
199 1
200
        return $contentCollection;
201
    }
202 1
203
    /**
204 3
     * Handles the content update migration action type
205
     *
206
     * @todo handle updating of more metadata fields
207 3
     */
208 3
    protected function update()
209 1
    {
210 1
        $contentService = $this->repository->getContentService();
211 1
        $contentTypeService = $this->repository->getContentTypeService();
212 1
213 1
        $contentCollection = $this->matchContents('update');
214 1
215 3
        if (count($contentCollection) > 1 && isset($this->dsl['references'])) {
216 3
            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");
217 3
        }
218
219 3
        $contentType = null;
220
221 3
        foreach ($contentCollection as $key => $content) {
0 ignored issues
show
Bug introduced by
The expression $contentCollection of type object<Kaliop\eZMigratio...ContentCollection>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
222
            $contentInfo = $content->contentInfo;
223
224
            if ($contentType == null) {
225
                $contentType = $contentTypeService->loadContentType($contentInfo->contentTypeId);
226
            }
227
228
            $contentUpdateStruct = $contentService->newContentUpdateStruct();
229
230
            if (isset($this->dsl['attributes'])) {
231 4
                $this->setFields($contentUpdateStruct, $this->dsl['attributes'], $contentType);
232
            }
233 4
234
            $versionCreator = null;
235
            if (isset($this->dsl['version_creator'])) {
236 4
                $versionCreator = $this->getUser($this->dsl['version_creator']);
237
            }
238 4
239
            $draft = $contentService->createContentDraft($contentInfo, null, $versionCreator);
240 4
            $contentService->updateContent($draft->versionInfo, $contentUpdateStruct);
241
            $content = $contentService->publishVersion($draft->versionInfo);
242 1
243 1
            if (isset($this->dsl['always_available']) ||
244
                isset($this->dsl['new_remote_id']) ||
245 4
                isset($this->dsl['owner']) ||
246 3
                isset($this->dsl['modification_date']) ||
247
                isset($this->dsl['publication_date'])) {
248 4
249 4
                $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
250 4
251
                if (isset($this->dsl['always_available'])) {
252
                    $contentMetaDataUpdateStruct->alwaysAvailable = $this->dsl['always_available'];
253
                }
254
255
                if (isset($this->dsl['new_remote_id'])) {
256
                    $contentMetaDataUpdateStruct->remoteId = $this->dsl['new_remote_id'];
257
                }
258
259 View Code Duplication
                if (isset($this->dsl['owner'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
260
                    $owner = $this->getUser($this->dsl['owner']);
261
                    $contentMetaDataUpdateStruct->ownerId = $owner->id;
262 4
                }
263
264 4
                if (isset($this->dsl['modification_date'])) {
265 4
                    $contentMetaDataUpdateStruct->modificationDate = $this->toDateTime($this->dsl['modification_date']);
266
                }
267
268 4
                if (isset($this->dsl['publication_date'])) {
269 4
                    $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($this->dsl['publication_date']);
270 4
                }
271
272 4
                $content = $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct);
273 1
            }
274 1
275
            if (isset($this->dsl['section'])) {
276 4
                $this->setSection($content, $this->dsl['section']);
277
            }
278
279
            if (isset($this->dsl['object_states'])) {
280
                $this->setObjectStates($content, $this->dsl['object_states']);
281
            }
282
283
            $contentCollection[$key] = $content;
284
        }
285
286
        $this->setReferences($contentCollection);
0 ignored issues
show
Bug introduced by
It seems like $contentCollection defined by $this->matchContents('update') on line 213 can be null; however, Kaliop\eZMigrationBundle...anager::setReferences() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
287 1
288
        return $contentCollection;
289 1
    }
290 1
291
    /**
292
     * Handles the content delete migration action type
293
     */
294
    protected function delete()
295
    {
296
        $contentService = $this->repository->getContentService();
297
298
        $contentCollection = $this->matchContents('delete');
299
300
        foreach ($contentCollection as $content) {
0 ignored issues
show
Bug introduced by
The expression $contentCollection of type object<Kaliop\eZMigratio...ContentCollection>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
301
            try {
302 3
                $contentService->deleteContent($content->contentInfo);
303
            } catch (NotFoundException $e) {
304 3
                // Someone else (or even us, by virtue of location tree?) removed the content which we found just a
305 2
                // second ago. We can safely ignore this
306
            }
307
        }
308 3
309
        return $contentCollection;
310
    }
311
312
    /**
313
     * @param string $action
314
     * @return ContentCollection
315 3
     * @throws \Exception
316
     */
317 3
    protected function matchContents($action)
318 3
    {
319 3
        if (!isset($this->dsl['object_id']) && !isset($this->dsl['remote_id']) && !isset($this->dsl['match'])) {
320 3
            throw new \Exception("The id or remote id of an object or a match condition is required to $action a location");
321 3
        }
322 3
323 2
        // Backwards compat
324 2
        if (!isset($this->dsl['match'])) {
325 2
            if (isset($this->dsl['object_id'])) {
326 1
                $this->dsl['match'] = array('content_id' => $this->dsl['object_id']);
327 1
            } elseif (isset($this->dsl['remote_id'])) {
328 1
                $this->dsl['match'] = array('content_remote_id' => $this->dsl['remote_id']);
329 1
            }
330
        }
331
332
        // convert the references passed in the match
333 3
        $match = $this->resolveReferencesRecursively($this->dsl['match']);
0 ignored issues
show
Deprecated Code introduced by
The method Kaliop\eZMigrationBundle...ReferencesRecursively() has been deprecated with message: will be moved into the reference resolver classes

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
334
335 3
        return $this->contentMatcher->match($match);
336 3
    }
337
338 3
    /**
339
     * Sets references to certain content attributes.
340
     *
341
     * @param \eZ\Publish\API\Repository\Values\Content\Content|ContentCollection $content
342
     * @throws \InvalidArgumentException When trying to set a reference to an unsupported attribute
343
     * @return boolean
344
     *
345
     * @todo add support for other attributes: contentTypeId, contentTypeIdentifier, section, etc... ?
346
     */
347
    protected function setReferences($content)
348
    {
349
        if (!array_key_exists('references', $this->dsl)) {
350
            return false;
351
        }
352
353
        if ($content instanceof ContentCollection) {
354
            if (count($content) > 1) {
355
                throw new \InvalidArgumentException('Content Manager does not support setting references for creating/updating of multiple contents');
356
            }
357
            $content = reset($content);
358
        }
359
360
        foreach ($this->dsl['references'] as $reference) {
361
362
            switch ($reference['attribute']) {
363
                case 'object_id':
364
                case 'content_id':
365
                case 'id':
366
                    $value = $content->id;
367
                    break;
368
                case 'remote_id':
369
                case 'content_remote_id':
370
                    $value = $content->contentInfo->remoteId;
371
                    break;
372
                case 'always_available':
373
                    $value = $content->contentInfo->alwaysAvailable;
374
                    break;
375
                case 'content_type_id':
376
                    $value = $content->contentInfo->contentTypeId;
377
                    break;
378 View Code Duplication
                case 'content_type_identifier':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
379
                    $contentTypeService = $this->repository->getContentTypeService();
380
                    $value = $contentTypeService->loadContentType($content->contentInfo->contentTypeId)->identifier;
381
                    break;
382
                case 'current_version':
383
                case 'current_version_no':
384
                    $value = $content->contentInfo->currentVersionNo;
385
                    break;
386
                case 'location_id':
387
                case 'main_location_id':
388
                    $value = $content->contentInfo->mainLocationId;
389
                    break;
390
                case 'main_language_code':
391
                    $value = $content->contentInfo->mainLanguageCode;
392
                    break;
393
                case 'modification_date':
394
                    $value = $content->contentInfo->modificationDate->getTimestamp();
395
                    break;
396
                case 'name':
397
                    $value = $content->contentInfo->name;
398
                    break;
399
                case 'owner_id':
400
                    $value = $content->contentInfo->ownerId;
401
                    break;
402
                case 'path':
403
                    $locationService = $this->repository->getLocationService();
404
                    $value = $locationService->loadLocation($content->contentInfo->mainLocationId)->pathString;
405
                    break;
406
                case 'publication_date':
407
                    $value = $content->contentInfo->publishedDate->getTimestamp();
408
                    break;
409
                case 'section_id':
410
                    $value = $content->contentInfo->sectionId;
411
                    break;
412
                default:
413
                    // allow to get the value of fields as well as their sub-parts
414
                    if (strpos($reference['attribute'], 'attributes.') === 0) {
415
                        $contentType = $this->repository->getContentTypeService()->loadContentType(
416
                            $content->contentInfo->contentTypeId
417
                        );
418
                        $parts = explode('.', $reference['attribute']);
419
                        $fieldIdentifier = $parts[1];
420
                        $field = $content->getField($fieldIdentifier);
421
                        $fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier);
422
                        $hashValue = $this->complexFieldManager->fieldValueToHash(
423
                            $fieldDefinition->fieldTypeIdentifier, $contentType->identifier, $field->value
424
                        );
425
                        if (is_array($hashValue) ) {
426 View Code Duplication
                            if (count($parts) == 2) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
427
                                throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute'] . ': the given attribute has an array value');
428
                            }
429
                            $value = JmesPath::search(implode('.', array_slice($parts, 2)), $hashValue);
430 View Code Duplication
                        } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
431
                            if (count($parts) > 2) {
432
                                throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute'] . ': the given attribute has a scalar value');
433
                            }
434
                            $value = $hashValue;
435
                        }
436
                        break;
437
                    }
438
439
                    throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute']);
440
            }
441
442
            $this->referenceResolver->addReference($reference['identifier'], $value);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Kaliop\eZMigrationBundle...erenceResolverInterface as the method addReference() does only exist in the following implementations of said interface: Kaliop\eZMigrationBundle...ver\ChainPrefixResolver, Kaliop\eZMigrationBundle...ver\ChainRegexpResolver, Kaliop\eZMigrationBundle...eResolver\ChainResolver, Kaliop\eZMigrationBundle...CustomReferenceResolver.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
443
        }
444
445
        return true;
446
    }
447
448
    /**
449
     * @param array $matchCondition
450
     * @param string $mode
451
     * @throws \Exception
452
     * @return array
453
     *
454
     * @todo add support for dumping all object languages
455
     * @todo add 2ndary locations when in 'update' mode
456
     * @todo add dumping of sort_field and sort_order for 2ndary locations
457
     */
458
    public function generateMigration(array $matchCondition, $mode)
459
    {
460
        $previousUserId = $this->loginUser(self::ADMIN_USER_ID);
461
        $contentCollection = $this->contentMatcher->match($matchCondition);
462
        $data = array();
463
464
        /** @var \eZ\Publish\API\Repository\Values\Content\Content $content */
465
        foreach ($contentCollection as $content) {
0 ignored issues
show
Bug introduced by
The expression $contentCollection of type object<Kaliop\eZMigratio...ContentCollection>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
466
467
            $location = $this->repository->getLocationService()->loadLocation($content->contentInfo->mainLocationId);
468
            $contentType = $this->repository->getContentTypeService()->loadContentType(
469
                $content->contentInfo->contentTypeId
470
            );
471
472
            $contentData = array(
473
                'type' => reset($this->supportedStepTypes),
474
                'mode' => $mode
475
            );
476
477
            switch ($mode) {
478
                case 'create':
479
                    $contentData = array_merge(
480
                        $contentData,
481
                        array(
482
                            'content_type' => $contentType->identifier,
483
                            'parent_location' => $location->parentLocationId,
484
                            'priority' => $location->priority,
485
                            'is_hidden' => $location->invisible,
486
                            'sort_field' => $this->sortConverter->sortField2Hash($location->sortField),
487
                            'sort_order' => $this->sortConverter->sortOrder2Hash($location->sortOrder),
488
                            'remote_id' => $content->contentInfo->remoteId,
489
                            'location_remote_id' => $location->remoteId
490
                        )
491
                    );
492
                    $locationService = $this->repository->getLocationService();
493
                    $locations = $locationService->loadLocations($content->contentInfo);
494
                    if (count($locations) > 1) {
495
                        $otherParentLocations = array();
496
                        foreach($locations as $otherLocation) {
497
                            if ($otherLocation->id != $location->id) {
498
                                $otherParentLocations[] = $otherLocation->parentLocationId;
499
                            }
500
                        }
501
                        $contentData['other_parent_locations'] = $otherParentLocations;
502
                    }
503
                    break;
504 View Code Duplication
                case 'update':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
505
                    $contentData = array_merge(
506
                        $contentData,
507
                        array(
508
                            'match' => array(
509
                                ContentMatcher::MATCH_CONTENT_REMOTE_ID => $content->contentInfo->remoteId
510
                            ),
511
                            'new_remote_id' => $content->contentInfo->remoteId,
512
                        )
513
                    );
514
                    break;
515 View Code Duplication
                case 'delete':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
516
                    $contentData = array_merge(
517
                        $contentData,
518
                        array(
519
                            'match' => array(
520
                                ContentMatcher::MATCH_CONTENT_REMOTE_ID => $content->contentInfo->remoteId
521
                            )
522
                        )
523
                    );
524
                    break;
525
                default:
526
                    throw new \Exception("Executor 'content' doesn't support mode '$mode'");
527
            }
528
529
            if ($mode != 'delete') {
530
531
                $attributes = array();
532
                foreach ($content->getFieldsByLanguage($this->getLanguageCode()) as $fieldIdentifier => $field) {
533
                    $fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier);
534
                    $attributes[$field->fieldDefIdentifier] = $this->complexFieldManager->fieldValueToHash(
535
                        $fieldDefinition->fieldTypeIdentifier, $contentType->identifier, $field->value
536
                    );
537
                }
538
539
                $contentData = array_merge(
540
                    $contentData,
541
                    array(
542
                        'lang' => $this->getLanguageCode(),
543
                        'section' => $content->contentInfo->sectionId,
544
                        'owner' => $content->contentInfo->ownerId,
545
                        'modification_date' => $content->contentInfo->modificationDate->getTimestamp(),
546
                        'publication_date' => $content->contentInfo->publishedDate->getTimestamp(),
547
                        'always_available' => (bool)$content->contentInfo->alwaysAvailable,
548
                        'attributes' => $attributes
549
                    )
550
                );
551
            }
552
553
            $data[] = $contentData;
554
        }
555
556
        $this->loginUser($previousUserId);
557
        return $data;
558
    }
559
560
    /**
561
     * Helper function to set the fields of a ContentCreateStruct based on the DSL attribute settings.
562
     *
563
     * @param ContentCreateStruct|ContentUpdateStruct $createOrUpdateStruct
564
     * @param ContentType $contentType
565
     * @param array $fields see description of expected format in code below
566
     * @throws \Exception
567
     */
568
    protected function setFields($createOrUpdateStruct, array $fields, ContentType $contentType)
569
    {
570
        $i = 0;
571
        // the 'easy' yml: key = field name, value = value
572
        // deprecated: the 'legacy' yml: key = numerical index, value = array ( field name => value )
573
        foreach ($fields as $key => $field) {
574
575
            if ($key === $i && is_array($field) && count($field) == 1) {
576
                // each $field is one key value pair
577
                // eg.: $field = array($fieldIdentifier => $fieldValue)
0 ignored issues
show
Unused Code Comprehensibility introduced by
48% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
578
                reset($field);
579
                $fieldIdentifier = key($field);
580
                $fieldValue = $field[$fieldIdentifier];
581
            } else {
582
                $fieldIdentifier = $key;
583
                $fieldValue = $field;
584
            }
585
586
            if (!isset($contentType->fieldDefinitionsByIdentifier[$fieldIdentifier])) {
0 ignored issues
show
Bug introduced by
The property fieldDefinitionsByIdentifier does not seem to exist. Did you mean fieldDefinitions?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
587
                throw new \Exception("Field '$fieldIdentifier' is not present in field type '{$contentType->identifier}'");
588
            }
589
590
            $fieldDefinition = $contentType->fieldDefinitionsByIdentifier[$fieldIdentifier];
0 ignored issues
show
Bug introduced by
The property fieldDefinitionsByIdentifier does not seem to exist. Did you mean fieldDefinitions?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
591
            $fieldValue = $this->getFieldValue($fieldValue, $fieldDefinition, $contentType->identifier, $this->context);
592
593
            $createOrUpdateStruct->setField($fieldIdentifier, $fieldValue, $this->getLanguageCode());
594
595
            $i++;
596
        }
597
    }
598
599 View Code Duplication
    protected function setSection(Content $content, $sectionKey)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
600
    {
601
        $sectionKey = $this->referenceResolver->resolveReference($sectionKey);
602
        $section = $this->sectionMatcher->matchOneByKey($sectionKey);
603
604
        $sectionService = $this->repository->getSectionService();
605
        $sectionService->assignSection($content->contentInfo, $section);
606
    }
607
608
    protected function setObjectStates(Content $content, array $stateKeys)
609
    {
610
        foreach ($stateKeys as $stateKey) {
611
            $stateKey = $this->referenceResolver->resolveReference($stateKey);
612
            /** @var \eZ\Publish\API\Repository\Values\ObjectState\ObjectState $state */
613
            $state = $this->objectStateMatcher->matchOneByKey($stateKey);
614
615
            $stateService = $this->repository->getObjectStateService();
616
            $stateService->setContentState($content->contentInfo, $state->getObjectStateGroup(), $state);
617
        }
618
    }
619
620
    /**
621
     * Create the field value from the migration definition hash
622
     *
623
     * @param mixed $value
624
     * @param FieldDefinition $fieldDefinition
625
     * @param string $contentTypeIdentifier
626
     * @param array $context
627
     * @throws \InvalidArgumentException
628
     * @return mixed
629
     */
630
    protected function getFieldValue($value, FieldDefinition $fieldDefinition, $contentTypeIdentifier, array $context = array())
631
    {
632
        $fieldTypeIdentifier = $fieldDefinition->fieldTypeIdentifier;
633
        if (is_array($value) || $this->complexFieldManager->managesField($fieldTypeIdentifier, $contentTypeIdentifier)) {
634
            // inject info about the current content type and field into the context
635
            $context['contentTypeIdentifier'] = $contentTypeIdentifier;
636
            $context['fieldIdentifier'] = $fieldDefinition->identifier;
637
            return $this->complexFieldManager->hashToFieldValue($fieldTypeIdentifier, $contentTypeIdentifier, $value, $context);
638
        }
639
640
        return $this->getSingleFieldValue($value, $fieldDefinition, $contentTypeIdentifier, $context);
641
    }
642
643
    /**
644
     * Create the field value for a primitive field from the migration definition hash
645
     *
646
     * @param mixed $value
647
     * @param FieldDefinition $fieldDefinition
648
     * @param string $contentTypeIdentifier
649
     * @param array $context
650
     * @throws \InvalidArgumentException
651
     * @return mixed
652
     */
653
    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.

This check looks from 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.

This check looks from 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.

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

Loading history...
654
    {
655
        // booleans were handled here. They are now handled as complextypes
656
657
        // q: do we really want this to happen by default on all scalar field values?
658
        // Note: if you want this *not* to happen, register a complex field for your scalar field...
659
        $value = $this->referenceResolver->resolveReference($value);
660
661
        return $value;
662
    }
663
664
    /**
665
     * Load user using either login, email, id - resolving eventual references
666
     * @param int|string $userKey
667
     * @return \eZ\Publish\API\Repository\Values\User\User
668
     */
669
    protected function getUser($userKey)
670
    {
671
        $userKey = $this->referenceResolver->resolveReference($userKey);
672
        return $this->userMatcher->matchOneByKey($userKey);
673
    }
674
675
    /**
676
     * @param int|string $date if integer, we assume a timestamp
677
     * @return \DateTime
678
     */
679
    protected function toDateTime($date)
680
    {
681
        if (is_int($date)) {
682
            return new \DateTime("@" . $date);
683
        } else {
684
            return new \DateTime($date);
685
        }
686
    }
687
}
688