Completed
Push — master ( be3b9c...dde077 )
by Gaetano
09:30
created

ContentManager   F

Complexity

Total Complexity 122

Size/Duplication

Total Lines 719
Duplicated Lines 10.57 %

Coupling/Cohesion

Components 1
Dependencies 28

Test Coverage

Coverage 84.08%

Importance

Changes 0
Metric Value
wmc 122
lcom 1
cbo 28
dl 76
loc 719
ccs 132
cts 157
cp 0.8408
rs 1.0434
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 1
F create() 9 133 25
A load() 0 8 1
F update() 4 92 24
A delete() 0 19 3
C matchContents() 23 23 7
D setReferences() 11 118 31
D generateMigration() 21 101 10
B setFields() 0 30 6
A setSection() 8 8 1
A setObjectStates() 0 11 2
A setMainLocation() 0 18 4
A getFieldValue() 0 12 3
A getSingleFieldValue() 0 10 1
A getUser() 0 5 1
A toDateTime() 0 8 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ContentManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ContentManager, and based on these observations, apply Extract Interface, too.

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 20
 */
28
class ContentManager extends RepositoryExecutor implements MigrationGeneratorInterface
29 20
{
30 20
    protected $supportedStepTypes = array('content');
31 20
    protected $supportedActions = array('create', 'load', 'update', 'delete');
32
33
    protected $contentMatcher;
34
    protected $sectionMatcher;
35
    protected $userMatcher;
36 4
    protected $objectStateMatcher;
37
    protected $objectStateGroupMatcher;
38 4
    protected $fieldHandlerManager;
39 4
    protected $locationManager;
40 4
    protected $sortConverter;
41
42 4
    public function __construct(
43 4
        ContentMatcher $contentMatcher,
44 3
        SectionMatcher $sectionMatcher,
45 3
        UserMatcher $userMatcher,
46 4
        ObjectStateMatcher $objectStateMatcher,
47
        ObjectStateGroupMatcher $objectStateGroupMatcher,
48 4
        FieldHandlerManager $fieldHandlerManager,
49 4
        LocationManager $locationManager,
50
        SortConverter $sortConverter
51 4
    ) {
52 1
        $this->contentMatcher = $contentMatcher;
53 1
        $this->sectionMatcher = $sectionMatcher;
54
        $this->userMatcher = $userMatcher;
55
        $this->objectStateMatcher = $objectStateMatcher;
56 4
        $this->objectStateGroupMatcher = $objectStateGroupMatcher;
57 4
        $this->fieldHandlerManager = $fieldHandlerManager;
58 1
        $this->locationManager = $locationManager;
59 1
        $this->sortConverter = $sortConverter;
60 4
    }
61 4
62 1
    /**
63 1
     * Handles the content create migration action type
64
     */
65 4
    protected function create($step)
66 1
    {
67 1
        $contentService = $this->repository->getContentService();
68
        $locationService = $this->repository->getLocationService();
69 4
        $contentTypeService = $this->repository->getContentTypeService();
70
71 4
        $contentTypeIdentifier = $step->dsl['content_type'];
72
        $contentTypeIdentifier = $this->referenceResolver->resolveReference($contentTypeIdentifier);
73
        /// @todo use a contenttypematcher
74
        $contentType = $contentTypeService->loadContentTypeByIdentifier($contentTypeIdentifier);
75
76
        $contentCreateStruct = $contentService->newContentCreateStruct($contentType, $this->getLanguageCode($step));
77
78
        $this->setFields($contentCreateStruct, $step->dsl['attributes'], $contentType, $step);
79
80
        if (isset($step->dsl['always_available'])) {
81
            $contentCreateStruct->alwaysAvailable = $step->dsl['always_available'];
82
        } else {
83 4
            // Could be removed when https://github.com/ezsystems/ezpublish-kernel/pull/1874 is merged,
84 3
            // but we strive to support old eZ kernel versions as well...
85
            $contentCreateStruct->alwaysAvailable = $contentType->defaultAlwaysAvailable;
86 3
        }
87 3
88
        if (isset($step->dsl['remote_id'])) {
89
            $contentCreateStruct->remoteId = $step->dsl['remote_id'];
90
        }
91
92 View Code Duplication
        if (isset($step->dsl['section'])) {
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...
93
            $sectionKey = $this->referenceResolver->resolveReference($step->dsl['section']);
94 1
            $section = $this->sectionMatcher->matchOneByKey($sectionKey);
95
            $contentCreateStruct->sectionId = $section->id;
96 1
        }
97 1
98 View Code Duplication
        if (isset($step->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...
99
            $owner = $this->getUser($step->dsl['owner']);
100
            $contentCreateStruct->ownerId = $owner->id;
101
        }
102
103
        // This is a bit tricky, as the eZPublish API does not support having a different creator and owner with only 1 version.
104
        // We allow it, hoping that nothing gets broken because of it
105
        if (isset($step->dsl['version_creator'])) {
106
            $realContentOwnerId = $contentCreateStruct->ownerId;
107
            if ($realContentOwnerId == null) {
108
                $realContentOwnerId = $this->repository->getCurrentUser()->id;
109
            }
110
            $versionCreator = $this->getUser($step->dsl['version_creator']);
111
            $contentCreateStruct->ownerId = $versionCreator->id;
112
        }
113
114
        if (isset($step->dsl['modification_date'])) {
115
            $contentCreateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']);
116
        }
117
118
        // instantiate a location create struct from the parent location:
119
        // BC
120
        $locationId = isset($step->dsl['parent_location']) ? $step->dsl['parent_location'] : (
121 1
            isset($step->dsl['main_location']) ? $step->dsl['main_location'] : null
122
        );
123 1
        // 1st resolve references
124
        $locationId = $this->referenceResolver->resolveReference($locationId);
125
        // 2nd allow to specify the location via remote_id
126
        $locationId = $this->locationManager->matchLocationByKey($locationId)->id;
127 1
        $locationCreateStruct = $locationService->newLocationCreateStruct($locationId);
128
129 1
        if (isset($step->dsl['location_remote_id'])) {
130 1
            $locationCreateStruct->remoteId = $step->dsl['location_remote_id'];
131
        }
132 1
133 1
        if (isset($step->dsl['priority'])) {
134 1
            $locationCreateStruct->priority = $step->dsl['priority'];
135
        }
136 1
137
        if (isset($step->dsl['is_hidden'])) {
138 1
            $locationCreateStruct->hidden = $step->dsl['is_hidden'];
139 1
        }
140 1
141
        if (isset($step->dsl['sort_field'])) {
142 1
            $locationCreateStruct->sortField = $this->sortConverter->hash2SortField($step->dsl['sort_field']);
143 1
        } else {
144 1
            $locationCreateStruct->sortField = $contentType->defaultSortField;
145
        }
146 1
147
        if (isset($step->dsl['sort_order'])) {
148
            $locationCreateStruct->sortOrder = $this->sortConverter->hash2SortOrder($step->dsl['sort_order']);
149
        } else {
150
            $locationCreateStruct->sortOrder = $contentType->defaultSortOrder;
151
        }
152
153
        $locations = array($locationCreateStruct);
154
155
        // BC
156
        $other_locations = isset($step->dsl['other_parent_locations']) ? $step->dsl['other_parent_locations'] : (
157
            isset($step->dsl['other_locations']) ? $step->dsl['other_locations'] : null
158
        );
159
        if (isset($other_locations)) {
160
            foreach ($other_locations as $locationId) {
161 1
                $locationId = $this->referenceResolver->resolveReference($locationId);
162 1
                $locationId = $this->locationManager->matchLocationByKey($locationId)->id;
163 1
                $secondaryLocationCreateStruct = $locationService->newLocationCreateStruct($locationId);
164
                array_push($locations, $secondaryLocationCreateStruct);
165
            }
166
        }
167
168 3
        // create a draft using the content and location create struct and publish it
169
        $draft = $contentService->createContent($contentCreateStruct, $locations);
170 3
        $content = $contentService->publishVersion($draft->versionInfo);
171
172 3
        if (isset($step->dsl['object_states'])) {
173
            $this->setObjectStates($content, $step->dsl['object_states']);
174 3
        }
175
176 3
        // 2nd part of the hack: re-set the content owner to its intended value
177 3
        if (isset($step->dsl['version_creator']) || isset($step->dsl['publication_date'])) {
178
            $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
179
180
            if (isset($step->dsl['version_creator'])) {
181 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...
182 3
            }
183
            if (isset($step->dsl['publication_date'])) {
184
                $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($step->dsl['publication_date']);
185
            }
186
            // we have to do this to make sure we preserve the custom modification date
187
            if (isset($this->dsl['modification_date'])) {
188
                $contentMetaDataUpdateStruct->modificationDate = $this->toDateTime($this->dsl['modification_date']);
0 ignored issues
show
Bug introduced by
The property dsl does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
189 3
            }
190
191 3
            $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct);
192
        }
193
194
        $this->setReferences($content, $step);
195
196 3
        return $content;
197 1
    }
198 1
199 1
    protected function load($step)
200
    {
201
        $contentCollection = $this->matchContents('load', $step);
202 1
203
        $this->setReferences($contentCollection, $step);
0 ignored issues
show
Bug introduced by
It seems like $contentCollection defined by $this->matchContents('load', $step) on line 201 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...
204 3
205
        return $contentCollection;
206
    }
207 3
208 3
    /**
209 1
     * Handles the content update migration action type
210 1
     *
211 1
     * @todo handle updating of more metadata fields
212 1
     */
213 1
    protected function update($step)
214 1
    {
215 3
        $contentService = $this->repository->getContentService();
216 3
        $contentTypeService = $this->repository->getContentTypeService();
217 3
218
        $contentCollection = $this->matchContents('update', $step);
219 3
220
        if (count($contentCollection) > 1 && isset($step->dsl['references'])) {
221 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");
222
        }
223
224
        if (count($contentCollection) > 1 && isset($step->dsl['main_location'])) {
225
            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");
226
        }
227
228
        $contentType = array();
229
230
        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...
231 4
            $contentInfo = $content->contentInfo;
232
233 4
            if (!isset($contentType[$contentInfo->contentTypeId])) {
234
                $contentType[$contentInfo->contentTypeId] = $contentTypeService->loadContentType($contentInfo->contentTypeId);
235
            }
236 4
237
            if (isset($step->dsl['attributes']) || isset($step->dsl['version_creator'])) {
238 4
                $contentUpdateStruct = $contentService->newContentUpdateStruct();
239
240 4
                if (isset($step->dsl['attributes'])) {
241
                    $this->setFields($contentUpdateStruct, $step->dsl['attributes'], $contentType[$contentInfo->contentTypeId], $step);
242 1
                }
243 1
244
                $versionCreator = null;
245 4
                if (isset($step->dsl['version_creator'])) {
246 3
                    $versionCreator = $this->getUser($step->dsl['version_creator']);
247
                }
248 4
249 4
                $draft = $contentService->createContentDraft($contentInfo, null, $versionCreator);
250 4
                $contentService->updateContent($draft->versionInfo, $contentUpdateStruct);
251
                $content = $contentService->publishVersion($draft->versionInfo);
252
            }
253
254
            if (isset($step->dsl['always_available']) ||
255
                isset($step->dsl['new_remote_id']) ||
256
                isset($step->dsl['owner']) ||
257
                isset($step->dsl['modification_date']) ||
258
                isset($step->dsl['publication_date'])) {
259
260
                $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
261
262 4
                if (isset($step->dsl['always_available'])) {
263
                    $contentMetaDataUpdateStruct->alwaysAvailable = $step->dsl['always_available'];
264 4
                }
265 4
266
                if (isset($step->dsl['new_remote_id'])) {
267
                    $contentMetaDataUpdateStruct->remoteId = $step->dsl['new_remote_id'];
268 4
                }
269 4
270 4 View Code Duplication
                if (isset($step->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...
271
                    $owner = $this->getUser($step->dsl['owner']);
272 4
                    $contentMetaDataUpdateStruct->ownerId = $owner->id;
273 1
                }
274 1
275
                if (isset($step->dsl['modification_date'])) {
276 4
                    $contentMetaDataUpdateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']);
277
                }
278
279
                if (isset($step->dsl['publication_date'])) {
280
                    $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($step->dsl['publication_date']);
281
                }
282
283
                $content = $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct);
284
            }
285
286
            if (isset($step->dsl['section'])) {
287 1
                $this->setSection($content, $step->dsl['section']);
288
            }
289 1
290 1
            if (isset($step->dsl['object_states'])) {
291
                $this->setObjectStates($content, $step->dsl['object_states']);
292
            }
293
294
            if (isset($step->dsl['main_location'])) {
295
                $this->setMainLocation($content, $step->dsl['main_location']);
296
297
            }
298
            $contentCollection[$key] = $content;
299
        }
300
301
        $this->setReferences($contentCollection, $step);
0 ignored issues
show
Bug introduced by
It seems like $contentCollection defined by $this->matchContents('update', $step) on line 218 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...
302 3
303
        return $contentCollection;
304 3
    }
305 2
306
    /**
307
     * Handles the content delete migration action type
308 3
     */
309
    protected function delete($step)
310
    {
311
        $contentCollection = $this->matchContents('delete', $step);
312
313
        $this->setReferences($contentCollection, $step);
0 ignored issues
show
Bug introduced by
It seems like $contentCollection defined by $this->matchContents('delete', $step) on line 311 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...
314
315 3
        $contentService = $this->repository->getContentService();
316
317 3
        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...
318 3
            try {
319 3
                $contentService->deleteContent($content->contentInfo);
320 3
            } catch (NotFoundException $e) {
321 3
                // Someone else (or even us, by virtue of location tree?) removed the content which we found just a
322 3
                // second ago. We can safely ignore this
323 2
            }
324 2
        }
325 2
326 1
        return $contentCollection;
327 1
    }
328 1
329 1
    /**
330
     * @param string $action
331
     * @return ContentCollection
332
     * @throws \Exception
333 3
     */
334 View Code Duplication
    protected function matchContents($action, $step)
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...
335 3
    {
336 3
        if (!isset($step->dsl['object_id']) && !isset($step->dsl['remote_id']) && !isset($step->dsl['match'])) {
337
            throw new \Exception("The id or remote id of an object or a match condition is required to $action a content");
338 3
        }
339
340
        // Backwards compat
341
342
        if (isset($step->dsl['match'])) {
343
            $match = $step->dsl['match'];
344
        } else {
345
            if (isset($step->dsl['object_id'])) {
346
                $match = array('content_id' => $step->dsl['object_id']);
347
            } elseif (isset($step->dsl['remote_id'])) {
348
                $match = array('content_remote_id' => $step->dsl['remote_id']);
349
            }
350
        }
351
352
        // convert the references passed in the match
353
        $match = $this->resolveReferencesRecursively($match);
0 ignored issues
show
Bug introduced by
The variable $match 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...
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...
354
355
        return $this->contentMatcher->match($match);
356
    }
357
358
    /**
359
     * Sets references to certain content attributes.
360
     *
361
     * @param \eZ\Publish\API\Repository\Values\Content\Content|ContentCollection $content
362
     * @throws \InvalidArgumentException When trying to set a reference to an unsupported attribute
363
     * @return boolean
364
     *
365
     * @todo add support for other attributes... ?
366
     */
367
    protected function setReferences($content, $step)
368
    {
369
        if (!array_key_exists('references', $step->dsl)) {
370
            return false;
371
        }
372
373
        $references = $this->setReferencesCommon($content, $step->dsl['references']);
374
        $content = $this->insureSingleEntity($content, $references);
375
376
        foreach ($references as $reference) {
377
378
            switch ($reference['attribute']) {
379
                case 'object_id':
380
                case 'content_id':
381
                case 'id':
382
                    $value = $content->id;
383
                    break;
384
                case 'remote_id':
385
                case 'content_remote_id':
386
                    $value = $content->contentInfo->remoteId;
387
                    break;
388
                case 'always_available':
389
                    $value = $content->contentInfo->alwaysAvailable;
390
                    break;
391
                case 'content_type_id':
392
                    $value = $content->contentInfo->contentTypeId;
393
                    break;
394 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...
395
                    $contentTypeService = $this->repository->getContentTypeService();
396
                    $value = $contentTypeService->loadContentType($content->contentInfo->contentTypeId)->identifier;
397
                    break;
398
                case 'current_version':
399
                case 'current_version_no':
400
                    $value = $content->contentInfo->currentVersionNo;
401
                    break;
402
                case 'location_id':
403
                case 'main_location_id':
404
                    $value = $content->contentInfo->mainLocationId;
405
                    break;
406
                case 'main_language_code':
407
                    $value = $content->contentInfo->mainLanguageCode;
408
                    break;
409
                case 'modification_date':
410
                    $value = $content->contentInfo->modificationDate->getTimestamp();
411
                    break;
412
                case 'name':
413
                    $value = $content->contentInfo->name;
414
                    break;
415
                case 'owner_id':
416
                    $value = $content->contentInfo->ownerId;
417
                    break;
418
                case 'path':
419
                    $locationService = $this->repository->getLocationService();
420
                    $value = $locationService->loadLocation($content->contentInfo->mainLocationId)->pathString;
421
                    break;
422
                case 'publication_date':
423
                    $value = $content->contentInfo->publishedDate->getTimestamp();
424
                    break;
425
                case 'section_id':
426
                    $value = $content->contentInfo->sectionId;
427
                    break;
428 View Code Duplication
                case 'section_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...
429
                    $sectionService = $this->repository->getSectionService();
430
                    $value = $sectionService->loadSection($content->contentInfo->sectionId)->identifier;
431
                    break;
432
                case 'version_count':
433
                    $contentService = $this->repository->getContentService();
434
                    $value = count($contentService->loadVersions($content->contentInfo));
435
                    break;
436
                default:
437
                    if (strpos($reference['attribute'], 'object_state.') === 0) {
438
                        $stateGroupKey = substr($reference['attribute'], 13);
439
                        $stateGroup = $this->objectStateGroupMatcher->matchOneByKey($stateGroupKey);
440
                        $value = $stateGroupKey . '/' . $this->repository->getObjectStateService()->
441
                            getContentState($content->contentInfo, $stateGroup)->identifier;
442
                        break;
443
                    }
444
445
                    // allow to get the value of fields as well as their sub-parts
446
                    if (strpos($reference['attribute'], 'attributes.') === 0) {
447
                        $contentType = $this->repository->getContentTypeService()->loadContentType(
448
                            $content->contentInfo->contentTypeId
449
                        );
450
                        $parts = explode('.', $reference['attribute']);
451
                        // totally not sure if this list of special chars is correct for what could follow a jmespath identifier...
452
                        // also what about quoted strings?
453
                        $fieldIdentifier = preg_replace('/[[(|&!{].*$/', '', $parts[1]);
454
                        $field = $content->getField($fieldIdentifier);
455
                        $fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier);
456
                        $hashValue = $this->fieldHandlerManager->fieldValueToHash(
457
                            $fieldDefinition->fieldTypeIdentifier, $contentType->identifier, $field->value
458
                        );
459
                        if (is_array($hashValue) ) {
460 View Code Duplication
                            if (count($parts) == 2 && $fieldIdentifier === $parts[1]) {
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...
461
                                throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute'] . ': the given attribute has an array value');
462
                            }
463
                            $value = JmesPath::search(implode('.', array_slice($parts, 1)), array($fieldIdentifier => $hashValue));
464
                        } else {
465
                            if (count($parts) > 2) {
466
                                throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute'] . ': the given attribute has a scalar value');
467
                            }
468
                            $value = $hashValue;
469
                        }
470
                        break;
471
                    }
472
473
                    throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute']);
474
            }
475
476
            $overwrite = false;
477
            if (isset($reference['overwrite'])) {
478
                $overwrite = $reference['overwrite'];
479
            }
480
            $this->referenceResolver->addReference($reference['identifier'], $value, $overwrite);
481
        }
482
483
        return true;
484
    }
485
486
    /**
487
     * @param array $matchCondition
488
     * @param string $mode
489
     * @param array $context
490
     * @throws \Exception
491
     * @return array
492
     *
493
     * @todo add support for dumping all object languages
494
     * @todo add 2ndary locations when in 'update' mode
495
     * @todo add dumping of sort_field and sort_order for 2ndary locations
496
     */
497
    public function generateMigration(array $matchCondition, $mode, array $context = array())
498
    {
499
        $previousUserId = $this->loginUser($this->getAdminUserIdentifierFromContext($context));
500
        $contentCollection = $this->contentMatcher->match($matchCondition);
501
        $data = array();
502
503
        /** @var \eZ\Publish\API\Repository\Values\Content\Content $content */
504
        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...
505
506
            $location = $this->repository->getLocationService()->loadLocation($content->contentInfo->mainLocationId);
507
            $contentType = $this->repository->getContentTypeService()->loadContentType(
508
                $content->contentInfo->contentTypeId
509
            );
510
511
            $contentData = array(
512
                'type' => reset($this->supportedStepTypes),
513
                'mode' => $mode
514
            );
515
516
            switch ($mode) {
517
                case 'create':
518
                    $contentData = array_merge(
519
                        $contentData,
520
                        array(
521
                            'content_type' => $contentType->identifier,
522
                            'parent_location' => $location->parentLocationId,
523
                            'priority' => $location->priority,
524
                            'is_hidden' => $location->invisible,
525
                            'sort_field' => $this->sortConverter->sortField2Hash($location->sortField),
526
                            'sort_order' => $this->sortConverter->sortOrder2Hash($location->sortOrder),
527
                            'remote_id' => $content->contentInfo->remoteId,
528
                            'location_remote_id' => $location->remoteId
529
                        )
530
                    );
531
                    $locationService = $this->repository->getLocationService();
532
                    $locations = $locationService->loadLocations($content->contentInfo);
533
                    if (count($locations) > 1) {
534
                        $otherParentLocations = array();
535
                        foreach($locations as $otherLocation) {
536
                            if ($otherLocation->id != $location->id) {
537
                                $otherParentLocations[] = $otherLocation->parentLocationId;
538
                            }
539
                        }
540
                        $contentData['other_parent_locations'] = $otherParentLocations;
541
                    }
542
                    break;
543 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...
544
                    $contentData = array_merge(
545
                        $contentData,
546
                        array(
547
                            'match' => array(
548
                                ContentMatcher::MATCH_CONTENT_REMOTE_ID => $content->contentInfo->remoteId
549
                            ),
550
                            'new_remote_id' => $content->contentInfo->remoteId,
551
                        )
552
                    );
553
                    break;
554 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...
555
                    $contentData = array_merge(
556
                        $contentData,
557
                        array(
558
                            'match' => array(
559
                                ContentMatcher::MATCH_CONTENT_REMOTE_ID => $content->contentInfo->remoteId
560
                            )
561
                        )
562
                    );
563
                    break;
564
                default:
565
                    throw new \Exception("Executor 'content' doesn't support mode '$mode'");
566
            }
567
568
            if ($mode != 'delete') {
569
570
                $attributes = array();
571
                foreach ($content->getFieldsByLanguage($this->getLanguageCodeFromContext($context)) as $fieldIdentifier => $field) {
572
                    $fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier);
573
                    $attributes[$field->fieldDefIdentifier] = $this->fieldHandlerManager->fieldValueToHash(
574
                        $fieldDefinition->fieldTypeIdentifier, $contentType->identifier, $field->value
575
                    );
576
                }
577
578
                $contentData = array_merge(
579
                    $contentData,
580
                    array(
581
                        'lang' => $this->getLanguageCodeFromContext($context),
582
                        'section' => $content->contentInfo->sectionId,
583
                        'owner' => $content->contentInfo->ownerId,
584
                        'modification_date' => $content->contentInfo->modificationDate->getTimestamp(),
585
                        'publication_date' => $content->contentInfo->publishedDate->getTimestamp(),
586
                        'always_available' => (bool)$content->contentInfo->alwaysAvailable,
587
                        'attributes' => $attributes
588
                    )
589
                );
590
            }
591
592
            $data[] = $contentData;
593
        }
594
595
        $this->loginUser($previousUserId);
596
        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
    protected function setFields($createOrUpdateStruct, array $fields, ContentType $contentType, $step)
609
    {
610
        $i = 0;
611
        // the 'easy' yml: key = field name, value = value
612
        // deprecated: the 'legacy' yml: key = numerical index, value = array ( field name => value )
613
        foreach ($fields as $key => $field) {
614
615
            if ($key === $i && is_array($field) && count($field) == 1) {
616
                // each $field is one key value pair
617
                // 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...
618
                reset($field);
619
                $fieldIdentifier = key($field);
620
                $fieldValue = $field[$fieldIdentifier];
621
            } else {
622
                $fieldIdentifier = $key;
623
                $fieldValue = $field;
624
            }
625
626
            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...
627
                throw new \Exception("Field '$fieldIdentifier' is not present in content type '{$contentType->identifier}'");
628
            }
629
630
            $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...
631
            $fieldValue = $this->getFieldValue($fieldValue, $fieldDefinition, $contentType->identifier, $step->context);
632
633
            $createOrUpdateStruct->setField($fieldIdentifier, $fieldValue, $this->getLanguageCode($step));
634
635
            $i++;
636
        }
637
    }
638
639 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...
640
    {
641
        $sectionKey = $this->referenceResolver->resolveReference($sectionKey);
642
        $section = $this->sectionMatcher->matchOneByKey($sectionKey);
643
644
        $sectionService = $this->repository->getSectionService();
645
        $sectionService->assignSection($content->contentInfo, $section);
646
    }
647
648
    protected function setObjectStates(Content $content, array $stateKeys)
649
    {
650
        foreach ($stateKeys as $stateKey) {
651
            $stateKey = $this->referenceResolver->resolveReference($stateKey);
652
            /** @var \eZ\Publish\API\Repository\Values\ObjectState\ObjectState $state */
653
            $state = $this->objectStateMatcher->matchOneByKey($stateKey);
654
655
            $stateService = $this->repository->getObjectStateService();
656
            $stateService->setContentState($content->contentInfo, $state->getObjectStateGroup(), $state);
657
        }
658
    }
659
660
    protected function setMainLocation(Content $content, $locationId)
661
    {
662
        $locationId = $this->referenceResolver->resolveReference($locationId);
663
        if (is_int($locationId) || ctype_digit($locationId)) {
664
            $location = $this->repository->getLocationService()->loadLocation($locationId);
665
        } else {
666
            $location = $this->repository->getLocationService()->loadLocationByRemoteId($locationId);
667
        }
668
669
        if ($location->contentInfo->id != $content->id) {
670
            throw new \Exception("Can not set main location {$location->id} to content {$content->id} as it belongs to another object");
671
        }
672
673
        $contentService = $this->repository->getContentService();
674
        $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
675
        $contentMetaDataUpdateStruct->mainLocationId = $location->id;
676
        $contentService->updateContentMetadata($location->contentInfo, $contentMetaDataUpdateStruct);
677
    }
678
679
    /**
680
     * Create the field value from the migration definition hash
681
     *
682
     * @param mixed $value
683
     * @param FieldDefinition $fieldDefinition
684
     * @param string $contentTypeIdentifier
685
     * @param array $context
686
     * @throws \InvalidArgumentException
687
     * @return mixed
688
     */
689
    protected function getFieldValue($value, FieldDefinition $fieldDefinition, $contentTypeIdentifier, array $context = array())
690
    {
691
        $fieldTypeIdentifier = $fieldDefinition->fieldTypeIdentifier;
692
        if (is_array($value) || $this->fieldHandlerManager->managesField($fieldTypeIdentifier, $contentTypeIdentifier)) {
693
            // inject info about the current content type and field into the context
694
            $context['contentTypeIdentifier'] = $contentTypeIdentifier;
695
            $context['fieldIdentifier'] = $fieldDefinition->identifier;
696
            return $this->fieldHandlerManager->hashToFieldValue($fieldTypeIdentifier, $contentTypeIdentifier, $value, $context);
697
        }
698
699
        return $this->getSingleFieldValue($value, $fieldDefinition, $contentTypeIdentifier, $context);
700
    }
701
702
    /**
703
     * Create the field value for a primitive field from the migration definition hash
704
     *
705
     * @param mixed $value
706
     * @param FieldDefinition $fieldDefinition
707
     * @param string $contentTypeIdentifier
708
     * @param array $context
709
     * @throws \InvalidArgumentException
710
     * @return mixed
711
     */
712
    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...
713
    {
714
        // booleans were handled here. They are now handled as complextypes
715
716
        // q: do we really want this to happen by default on all scalar field values?
717
        // Note: if you want this *not* to happen, register a complex field for your scalar field...
718
        $value = $this->referenceResolver->resolveReference($value);
719
720
        return $value;
721
    }
722
723
    /**
724
     * Load user using either login, email, id - resolving eventual references
725
     * @param int|string $userKey
726
     * @return \eZ\Publish\API\Repository\Values\User\User
727
     */
728
    protected function getUser($userKey)
729
    {
730
        $userKey = $this->referenceResolver->resolveReference($userKey);
731
        return $this->userMatcher->matchOneByKey($userKey);
732
    }
733
734
    /**
735
     * @param int|string $date if integer, we assume a timestamp
736
     * @return \DateTime
737
     */
738
    protected function toDateTime($date)
739
    {
740
        if (is_int($date)) {
741
            return new \DateTime("@" . $date);
742
        } else {
743
            return new \DateTime($date);
744
        }
745
    }
746
}
747