Completed
Push — master ( ac3b8b...3c857d )
by Gaetano
18:21
created

ContentManager::matchContents()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 23
Code Lines 12

Duplication

Lines 23
Ratio 100 %

Code Coverage

Tests 12
CRAP Score 7.1429

Importance

Changes 0
Metric Value
dl 23
loc 23
c 0
b 0
f 0
rs 6.7272
ccs 12
cts 14
cp 0.8571
cc 7
eloc 12
nc 5
nop 2
crap 7.1429
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\FieldHandlerManager;
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 $fieldHandlerManager;
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
        FieldHandlerManager $fieldHandlerManager,
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->fieldHandlerManager = $fieldHandlerManager;
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($step)
61 4
    {
62 1
        $contentService = $this->repository->getContentService();
63 1
        $locationService = $this->repository->getLocationService();
64
        $contentTypeService = $this->repository->getContentTypeService();
65 4
66 1
        $contentTypeIdentifier = $step->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($step));
72
73
        $this->setFields($contentCreateStruct, $step->dsl['attributes'], $contentType, $step);
74
75
        if (isset($step->dsl['always_available'])) {
76
            $contentCreateStruct->alwaysAvailable = $step->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($step->dsl['remote_id'])) {
84 3
            $contentCreateStruct->remoteId = $step->dsl['remote_id'];
85
        }
86 3
87 3 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...
88
            $sectionKey = $this->referenceResolver->resolveReference($step->dsl['section']);
89
            $section = $this->sectionMatcher->matchOneByKey($sectionKey);
90
            $contentCreateStruct->sectionId = $section->id;
91
        }
92
93 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...
94 1
            $owner = $this->getUser($step->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($step->dsl['version_creator'])) {
101
            $realContentOwnerId = $contentCreateStruct->ownerId;
102
            if ($realContentOwnerId == null) {
103
                $realContentOwnerId = $this->repository->getCurrentUser()->id;
104
            }
105
            $versionCreator = $this->getUser($step->dsl['version_creator']);
106
            $contentCreateStruct->ownerId = $versionCreator->id;
107
        }
108
109
        if (isset($step->dsl['modification_date'])) {
110
            $contentCreateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']);
111
        }
112
113
        // instantiate a location create struct from the parent location:
114
        // BC
115
        $locationId = isset($step->dsl['parent_location']) ? $step->dsl['parent_location'] : (
116
            isset($step->dsl['main_location']) ? $step->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($step->dsl['location_remote_id'])) {
125
            $locationCreateStruct->remoteId = $step->dsl['location_remote_id'];
126
        }
127 1
128
        if (isset($step->dsl['priority'])) {
129 1
            $locationCreateStruct->priority = $step->dsl['priority'];
130 1
        }
131
132 1
        if (isset($step->dsl['is_hidden'])) {
133 1
            $locationCreateStruct->hidden = $step->dsl['is_hidden'];
134 1
        }
135
136 1
        if (isset($step->dsl['sort_field'])) {
137
            $locationCreateStruct->sortField = $this->sortConverter->hash2SortField($step->dsl['sort_field']);
138 1
        } else {
139 1
            $locationCreateStruct->sortField = $contentType->defaultSortField;
140 1
        }
141
142 1
        if (isset($step->dsl['sort_order'])) {
143 1
            $locationCreateStruct->sortOrder = $this->sortConverter->hash2SortOrder($step->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($step->dsl['other_parent_locations']) ? $step->dsl['other_parent_locations'] : (
152
            isset($step->dsl['other_locations']) ? $step->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($step->dsl['object_states'])) {
168 3
            $this->setObjectStates($content, $step->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($step->dsl['version_creator']) || isset($step->dsl['publication_date'])) {
173
            $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
174 3
175
            if (isset($step->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($step->dsl['publication_date'])) {
179
                $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($step->dsl['publication_date']);
180
            }
181 3
            // we have to do this to make sure we preserve the custom modification date
182 3
            if (isset($this->dsl['modification_date'])) {
183
                $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...
184
            }
185
186
            $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct);
187
        }
188
189 3
        $this->setReferences($content, $step);
190
191 3
        return $content;
192
    }
193
194
    protected function load($step)
195
    {
196 3
        $contentCollection = $this->matchContents('load', $step);
197 1
198 1
        // This check is already done in setReferences
199 1
        /*if (count($contentCollection) > 1 && isset($step->dsl['references'])) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% 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...
200
            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");
201
        }*/
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 196 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
        $contentType = null;
225
226
        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...
227
            $contentInfo = $content->contentInfo;
228
229
            if ($contentType == null) {
230
                $contentType = $contentTypeService->loadContentType($contentInfo->contentTypeId);
231 4
            }
232
233 4
            $contentUpdateStruct = $contentService->newContentUpdateStruct();
234
235
            if (isset($step->dsl['attributes'])) {
236 4
                $this->setFields($contentUpdateStruct, $step->dsl['attributes'], $contentType, $step);
237
            }
238 4
239
            $versionCreator = null;
240 4
            if (isset($step->dsl['version_creator'])) {
241
                $versionCreator = $this->getUser($step->dsl['version_creator']);
242 1
            }
243 1
244
            $draft = $contentService->createContentDraft($contentInfo, null, $versionCreator);
245 4
            $contentService->updateContent($draft->versionInfo, $contentUpdateStruct);
246 3
            $content = $contentService->publishVersion($draft->versionInfo);
247
248 4
            if (isset($step->dsl['always_available']) ||
249 4
                isset($step->dsl['new_remote_id']) ||
250 4
                isset($step->dsl['owner']) ||
251
                isset($step->dsl['modification_date']) ||
252
                isset($step->dsl['publication_date'])) {
253
254
                $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
255
256
                if (isset($step->dsl['always_available'])) {
257
                    $contentMetaDataUpdateStruct->alwaysAvailable = $step->dsl['always_available'];
258
                }
259
260
                if (isset($step->dsl['new_remote_id'])) {
261
                    $contentMetaDataUpdateStruct->remoteId = $step->dsl['new_remote_id'];
262 4
                }
263
264 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...
265 4
                    $owner = $this->getUser($step->dsl['owner']);
266
                    $contentMetaDataUpdateStruct->ownerId = $owner->id;
267
                }
268 4
269 4
                if (isset($step->dsl['modification_date'])) {
270 4
                    $contentMetaDataUpdateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']);
271
                }
272 4
273 1
                if (isset($step->dsl['publication_date'])) {
274 1
                    $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($step->dsl['publication_date']);
275
                }
276 4
277
                $content = $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct);
278
            }
279
280
            if (isset($step->dsl['section'])) {
281
                $this->setSection($content, $step->dsl['section']);
282
            }
283
284
            if (isset($step->dsl['object_states'])) {
285
                $this->setObjectStates($content, $step->dsl['object_states']);
286
            }
287 1
288
            $contentCollection[$key] = $content;
289 1
        }
290 1
291
        $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...
292
293
        return $contentCollection;
294
    }
295
296
    /**
297
     * Handles the content delete migration action type
298
     */
299
    protected function delete($step)
300
    {
301
        $contentService = $this->repository->getContentService();
302 3
303
        $contentCollection = $this->matchContents('delete', $step);
304 3
305 2
        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...
306
            try {
307
                $contentService->deleteContent($content->contentInfo);
308 3
            } catch (NotFoundException $e) {
309
                // Someone else (or even us, by virtue of location tree?) removed the content which we found just a
310
                // second ago. We can safely ignore this
311
            }
312
        }
313
314
        return $contentCollection;
315 3
    }
316
317 3
    /**
318 3
     * @param string $action
319 3
     * @return ContentCollection
320 3
     * @throws \Exception
321 3
     */
322 3 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...
323 2
    {
324 2
        if (!isset($step->dsl['object_id']) && !isset($step->dsl['remote_id']) && !isset($step->dsl['match'])) {
325 2
            throw new \Exception("The id or remote id of an object or a match condition is required to $action a location");
326 1
        }
327 1
328 1
        // Backwards compat
329 1
330
        if (isset($step->dsl['match'])) {
331
            $match = $step->dsl['match'];
332
        } else {
333 3
            if (isset($step->dsl['object_id'])) {
334
                $match = array('content_id' => $step->dsl['object_id']);
335 3
            } elseif (isset($step->dsl['remote_id'])) {
336 3
                $match = array('content_remote_id' => $step->dsl['remote_id']);
337
            }
338 3
        }
339
340
        // convert the references passed in the match
341
        $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...
342
343
        return $this->contentMatcher->match($match);
344
    }
345
346
    /**
347
     * Sets references to certain content attributes.
348
     *
349
     * @param \eZ\Publish\API\Repository\Values\Content\Content|ContentCollection $content
350
     * @throws \InvalidArgumentException When trying to set a reference to an unsupported attribute
351
     * @return boolean
352
     *
353
     * @todo add support for other attributes: object_states etc... ?
354
     */
355
    protected function setReferences($content, $step)
356
    {
357
        if (!array_key_exists('references', $step->dsl)) {
358
            return false;
359
        }
360
361
        if ($content instanceof ContentCollection) {
362
            if (count($content) > 1) {
363
                throw new \InvalidArgumentException('Content Manager does not support setting references for creating/updating/loading of multiple contents');
364
            }
365
            if (count($content) == 0) {
366
                throw new \InvalidArgumentException('Content Manager does not support setting references for creating/updating/loading of no contents');
367
            }
368
            $content = reset($content);
369
        }
370
371
        foreach ($step->dsl['references'] as $reference) {
372
373
            switch ($reference['attribute']) {
374
                case 'object_id':
375
                case 'content_id':
376
                case 'id':
377
                    $value = $content->id;
378
                    break;
379
                case 'remote_id':
380
                case 'content_remote_id':
381
                    $value = $content->contentInfo->remoteId;
382
                    break;
383
                case 'always_available':
384
                    $value = $content->contentInfo->alwaysAvailable;
385
                    break;
386
                case 'content_type_id':
387
                    $value = $content->contentInfo->contentTypeId;
388
                    break;
389 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...
390
                    $contentTypeService = $this->repository->getContentTypeService();
391
                    $value = $contentTypeService->loadContentType($content->contentInfo->contentTypeId)->identifier;
392
                    break;
393
                case 'current_version':
394
                case 'current_version_no':
395
                    $value = $content->contentInfo->currentVersionNo;
396
                    break;
397
                case 'location_id':
398
                case 'main_location_id':
399
                    $value = $content->contentInfo->mainLocationId;
400
                    break;
401
                case 'main_language_code':
402
                    $value = $content->contentInfo->mainLanguageCode;
403
                    break;
404
                case 'modification_date':
405
                    $value = $content->contentInfo->modificationDate->getTimestamp();
406
                    break;
407
                case 'name':
408
                    $value = $content->contentInfo->name;
409
                    break;
410
                case 'owner_id':
411
                    $value = $content->contentInfo->ownerId;
412
                    break;
413
                case 'path':
414
                    $locationService = $this->repository->getLocationService();
415
                    $value = $locationService->loadLocation($content->contentInfo->mainLocationId)->pathString;
416
                    break;
417
                case 'publication_date':
418
                    $value = $content->contentInfo->publishedDate->getTimestamp();
419
                    break;
420
                case 'section_id':
421
                    $value = $content->contentInfo->sectionId;
422
                    break;
423 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...
424
                    $sectionService = $this->repository->getSectionService();
425
                    $value = $sectionService->loadSection($content->contentInfo->sectionId)->identifier;
426
                    break;
427
                default:
428
                    // allow to get the value of fields as well as their sub-parts
429
                    if (strpos($reference['attribute'], 'attributes.') === 0) {
430
                        $contentType = $this->repository->getContentTypeService()->loadContentType(
431
                            $content->contentInfo->contentTypeId
432
                        );
433
                        $parts = explode('.', $reference['attribute']);
434
                        // totally not sure if this list of special chars is correct for what could follow a jmespath identifier...
435
                        // also what about quoted strings?
436
                        $fieldIdentifier = preg_replace('/[[(|&!{].*$/', '', $parts[1]);
437
                        $field = $content->getField($fieldIdentifier);
438
                        $fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier);
439
                        $hashValue = $this->fieldHandlerManager->fieldValueToHash(
440
                            $fieldDefinition->fieldTypeIdentifier, $contentType->identifier, $field->value
441
                        );
442
                        if (is_array($hashValue) ) {
443 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...
444
                                throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute'] . ': the given attribute has an array value');
445
                            }
446
                            $value = JmesPath::search(implode('.', array_slice($parts, 1)), array($fieldIdentifier => $hashValue));
447
                        } else {
448
                            if (count($parts) > 2) {
449
                                throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute'] . ': the given attribute has a scalar value');
450
                            }
451
                            $value = $hashValue;
452
                        }
453
                        break;
454
                    }
455
456
                    throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute']);
457
            }
458
459
            $overwrite = false;
460
            if (isset($reference['overwrite'])) {
461
                $overwrite = $reference['overwrite'];
462
            }
463
            $this->referenceResolver->addReference($reference['identifier'], $value, $overwrite);
464
        }
465
466
        return true;
467
    }
468
469
    /**
470
     * @param array $matchCondition
471
     * @param string $mode
472
     * @param array $context
473
     * @throws \Exception
474
     * @return array
475
     *
476
     * @todo add support for dumping all object languages
477
     * @todo add 2ndary locations when in 'update' mode
478
     * @todo add dumping of sort_field and sort_order for 2ndary locations
479
     */
480
    public function generateMigration(array $matchCondition, $mode, array $context = array())
481
    {
482
        $previousUserId = $this->loginUser($this->getAdminUserIdentifierFromContext($context));
483
        $contentCollection = $this->contentMatcher->match($matchCondition);
484
        $data = array();
485
486
        /** @var \eZ\Publish\API\Repository\Values\Content\Content $content */
487
        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...
488
489
            $location = $this->repository->getLocationService()->loadLocation($content->contentInfo->mainLocationId);
490
            $contentType = $this->repository->getContentTypeService()->loadContentType(
491
                $content->contentInfo->contentTypeId
492
            );
493
494
            $contentData = array(
495
                'type' => reset($this->supportedStepTypes),
496
                'mode' => $mode
497
            );
498
499
            switch ($mode) {
500
                case 'create':
501
                    $contentData = array_merge(
502
                        $contentData,
503
                        array(
504
                            'content_type' => $contentType->identifier,
505
                            'parent_location' => $location->parentLocationId,
506
                            'priority' => $location->priority,
507
                            'is_hidden' => $location->invisible,
508
                            'sort_field' => $this->sortConverter->sortField2Hash($location->sortField),
509
                            'sort_order' => $this->sortConverter->sortOrder2Hash($location->sortOrder),
510
                            'remote_id' => $content->contentInfo->remoteId,
511
                            'location_remote_id' => $location->remoteId
512
                        )
513
                    );
514
                    $locationService = $this->repository->getLocationService();
515
                    $locations = $locationService->loadLocations($content->contentInfo);
516
                    if (count($locations) > 1) {
517
                        $otherParentLocations = array();
518
                        foreach($locations as $otherLocation) {
519
                            if ($otherLocation->id != $location->id) {
520
                                $otherParentLocations[] = $otherLocation->parentLocationId;
521
                            }
522
                        }
523
                        $contentData['other_parent_locations'] = $otherParentLocations;
524
                    }
525
                    break;
526 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...
527
                    $contentData = array_merge(
528
                        $contentData,
529
                        array(
530
                            'match' => array(
531
                                ContentMatcher::MATCH_CONTENT_REMOTE_ID => $content->contentInfo->remoteId
532
                            ),
533
                            'new_remote_id' => $content->contentInfo->remoteId,
534
                        )
535
                    );
536
                    break;
537 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...
538
                    $contentData = array_merge(
539
                        $contentData,
540
                        array(
541
                            'match' => array(
542
                                ContentMatcher::MATCH_CONTENT_REMOTE_ID => $content->contentInfo->remoteId
543
                            )
544
                        )
545
                    );
546
                    break;
547
                default:
548
                    throw new \Exception("Executor 'content' doesn't support mode '$mode'");
549
            }
550
551
            if ($mode != 'delete') {
552
553
                $attributes = array();
554
                foreach ($content->getFieldsByLanguage($this->getLanguageCodeFromContext($context)) as $fieldIdentifier => $field) {
555
                    $fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier);
556
                    $attributes[$field->fieldDefIdentifier] = $this->fieldHandlerManager->fieldValueToHash(
557
                        $fieldDefinition->fieldTypeIdentifier, $contentType->identifier, $field->value
558
                    );
559
                }
560
561
                $contentData = array_merge(
562
                    $contentData,
563
                    array(
564
                        'lang' => $this->getLanguageCodeFromContext($context),
565
                        'section' => $content->contentInfo->sectionId,
566
                        'owner' => $content->contentInfo->ownerId,
567
                        'modification_date' => $content->contentInfo->modificationDate->getTimestamp(),
568
                        'publication_date' => $content->contentInfo->publishedDate->getTimestamp(),
569
                        'always_available' => (bool)$content->contentInfo->alwaysAvailable,
570
                        'attributes' => $attributes
571
                    )
572
                );
573
            }
574
575
            $data[] = $contentData;
576
        }
577
578
        $this->loginUser($previousUserId);
579
        return $data;
580
    }
581
582
    /**
583
     * Helper function to set the fields of a ContentCreateStruct based on the DSL attribute settings.
584
     *
585
     * @param ContentCreateStruct|ContentUpdateStruct $createOrUpdateStruct
586
     * @param array $fields see description of expected format in code below
587
     * @param ContentType $contentType
588
     * @param $step
589
     * @throws \Exception
590
     */
591
    protected function setFields($createOrUpdateStruct, array $fields, ContentType $contentType, $step)
592
    {
593
        $i = 0;
594
        // the 'easy' yml: key = field name, value = value
595
        // deprecated: the 'legacy' yml: key = numerical index, value = array ( field name => value )
596
        foreach ($fields as $key => $field) {
597
598
            if ($key === $i && is_array($field) && count($field) == 1) {
599
                // each $field is one key value pair
600
                // 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...
601
                reset($field);
602
                $fieldIdentifier = key($field);
603
                $fieldValue = $field[$fieldIdentifier];
604
            } else {
605
                $fieldIdentifier = $key;
606
                $fieldValue = $field;
607
            }
608
609
            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...
610
                throw new \Exception("Field '$fieldIdentifier' is not present in field type '{$contentType->identifier}'");
611
            }
612
613
            $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...
614
            $fieldValue = $this->getFieldValue($fieldValue, $fieldDefinition, $contentType->identifier, $step->context);
615
616
            $createOrUpdateStruct->setField($fieldIdentifier, $fieldValue, $this->getLanguageCode($step));
617
618
            $i++;
619
        }
620
    }
621
622 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...
623
    {
624
        $sectionKey = $this->referenceResolver->resolveReference($sectionKey);
625
        $section = $this->sectionMatcher->matchOneByKey($sectionKey);
626
627
        $sectionService = $this->repository->getSectionService();
628
        $sectionService->assignSection($content->contentInfo, $section);
629
    }
630
631
    protected function setObjectStates(Content $content, array $stateKeys)
632
    {
633
        foreach ($stateKeys as $stateKey) {
634
            $stateKey = $this->referenceResolver->resolveReference($stateKey);
635
            /** @var \eZ\Publish\API\Repository\Values\ObjectState\ObjectState $state */
636
            $state = $this->objectStateMatcher->matchOneByKey($stateKey);
637
638
            $stateService = $this->repository->getObjectStateService();
639
            $stateService->setContentState($content->contentInfo, $state->getObjectStateGroup(), $state);
640
        }
641
    }
642
643
    /**
644
     * Create the field value 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 getFieldValue($value, FieldDefinition $fieldDefinition, $contentTypeIdentifier, array $context = array())
654
    {
655
        $fieldTypeIdentifier = $fieldDefinition->fieldTypeIdentifier;
656
        if (is_array($value) || $this->fieldHandlerManager->managesField($fieldTypeIdentifier, $contentTypeIdentifier)) {
657
            // inject info about the current content type and field into the context
658
            $context['contentTypeIdentifier'] = $contentTypeIdentifier;
659
            $context['fieldIdentifier'] = $fieldDefinition->identifier;
660
            return $this->fieldHandlerManager->hashToFieldValue($fieldTypeIdentifier, $contentTypeIdentifier, $value, $context);
661
        }
662
663
        return $this->getSingleFieldValue($value, $fieldDefinition, $contentTypeIdentifier, $context);
664
    }
665
666
    /**
667
     * Create the field value for a primitive field from the migration definition hash
668
     *
669
     * @param mixed $value
670
     * @param FieldDefinition $fieldDefinition
671
     * @param string $contentTypeIdentifier
672
     * @param array $context
673
     * @throws \InvalidArgumentException
674
     * @return mixed
675
     */
676
    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...
677
    {
678
        // booleans were handled here. They are now handled as complextypes
679
680
        // q: do we really want this to happen by default on all scalar field values?
681
        // Note: if you want this *not* to happen, register a complex field for your scalar field...
682
        $value = $this->referenceResolver->resolveReference($value);
683
684
        return $value;
685
    }
686
687
    /**
688
     * Load user using either login, email, id - resolving eventual references
689
     * @param int|string $userKey
690
     * @return \eZ\Publish\API\Repository\Values\User\User
691
     */
692
    protected function getUser($userKey)
693
    {
694
        $userKey = $this->referenceResolver->resolveReference($userKey);
695
        return $this->userMatcher->matchOneByKey($userKey);
696
    }
697
698
    /**
699
     * @param int|string $date if integer, we assume a timestamp
700
     * @return \DateTime
701
     */
702
    protected function toDateTime($date)
703
    {
704
        if (is_int($date)) {
705
            return new \DateTime("@" . $date);
706
        } else {
707
            return new \DateTime($date);
708
        }
709
    }
710
}
711