Completed
Push — master ( 7691d1...0c804e )
by Gaetano
42:12
created

ContentManager::getContentLanguageCodes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 1
cts 1
cp 1
rs 9.7998
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Core\Executor;
4
5
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
6
use eZ\Publish\API\Repository\Values\ContentType\FieldDefinition;
7
use eZ\Publish\API\Repository\Values\Content\Location;
8
use eZ\Publish\API\Repository\Values\Content\Content;
9
use eZ\Publish\API\Repository\Values\Content\ContentCreateStruct;
10
use eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct;
11
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
12
use Kaliop\eZMigrationBundle\API\Collection\ContentCollection;
13
use Kaliop\eZMigrationBundle\API\MigrationGeneratorInterface;
14
use Kaliop\eZMigrationBundle\Core\FieldHandlerManager;
15
use Kaliop\eZMigrationBundle\Core\Matcher\ContentMatcher;
16
use Kaliop\eZMigrationBundle\Core\Matcher\SectionMatcher;
17
use Kaliop\eZMigrationBundle\Core\Matcher\UserMatcher;
18
use Kaliop\eZMigrationBundle\Core\Matcher\ObjectStateMatcher;
19
use Kaliop\eZMigrationBundle\Core\Matcher\ObjectStateGroupMatcher;
20
use Kaliop\eZMigrationBundle\Core\Helper\SortConverter;
21
use JmesPath\Env as JmesPath;
22
23
/**
24
 * Handles content migrations.
25
 *
26
 * @todo add support for updating of content metadata
27
 */
28
class ContentManager extends RepositoryExecutor implements MigrationGeneratorInterface
29
{
30
    protected $supportedStepTypes = array('content');
31
    protected $supportedActions = array('create', 'load', 'update', 'delete');
32
33
    protected $contentMatcher;
34
    protected $sectionMatcher;
35
    protected $userMatcher;
36
    protected $objectStateMatcher;
37
    protected $objectStateGroupMatcher;
38
    protected $fieldHandlerManager;
39
    protected $locationManager;
40
    protected $sortConverter;
41
42 76
    public function __construct(
43
        ContentMatcher $contentMatcher,
44
        SectionMatcher $sectionMatcher,
45
        UserMatcher $userMatcher,
46
        ObjectStateMatcher $objectStateMatcher,
47
        ObjectStateGroupMatcher $objectStateGroupMatcher,
48
        FieldHandlerManager $fieldHandlerManager,
49
        LocationManager $locationManager,
50
        SortConverter $sortConverter
51
    ) {
52 76
        $this->contentMatcher = $contentMatcher;
53 76
        $this->sectionMatcher = $sectionMatcher;
54 76
        $this->userMatcher = $userMatcher;
55 76
        $this->objectStateMatcher = $objectStateMatcher;
56 76
        $this->objectStateGroupMatcher = $objectStateGroupMatcher;
57 76
        $this->fieldHandlerManager = $fieldHandlerManager;
58 76
        $this->locationManager = $locationManager;
59 76
        $this->sortConverter = $sortConverter;
60 76
    }
61
62
    /**
63
     * Handles the content create migration action type
64
     */
65 11
    protected function create($step)
66
    {
67 11
        $contentService = $this->repository->getContentService();
68 11
        $locationService = $this->repository->getLocationService();
69 11
        $contentTypeService = $this->repository->getContentTypeService();
70
71 11
        $contentTypeIdentifier = $step->dsl['content_type'];
72 11
        $contentTypeIdentifier = $this->referenceResolver->resolveReference($contentTypeIdentifier);
73
        /// @todo use a contenttypematcher
74 11
        $contentType = $contentTypeService->loadContentTypeByIdentifier($contentTypeIdentifier);
75
76 11
        $contentCreateStruct = $contentService->newContentCreateStruct($contentType, $this->getLanguageCode($step));
77
78 11
        $this->setFields($contentCreateStruct, $step->dsl['attributes'], $contentType, $step);
79
80 11
        if (isset($step->dsl['always_available'])) {
81
            $contentCreateStruct->alwaysAvailable = $step->dsl['always_available'];
82
        } else {
83
            // Could be removed when https://github.com/ezsystems/ezpublish-kernel/pull/1874 is merged,
84
            // but we strive to support old eZ kernel versions as well...
85 11
            $contentCreateStruct->alwaysAvailable = $contentType->defaultAlwaysAvailable;
86
        }
87
88 11
        if (isset($step->dsl['remote_id'])) {
89 3
            $contentCreateStruct->remoteId = $step->dsl['remote_id'];
90
        }
91
92 11 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 1
            $sectionKey = $this->referenceResolver->resolveReference($step->dsl['section']);
94 1
            $section = $this->sectionMatcher->matchOneByKey($sectionKey);
95 1
            $contentCreateStruct->sectionId = $section->id;
96
        }
97
98 11 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 2
            $owner = $this->getUser($step->dsl['owner']);
100 2
            $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 11
        if (isset($step->dsl['version_creator'])) {
106 1
            $realContentOwnerId = $contentCreateStruct->ownerId;
107 1
            if ($realContentOwnerId == null) {
108 1
                $realContentOwnerId = $this->repository->getCurrentUser()->id;
109
            }
110 1
            $versionCreator = $this->getUser($step->dsl['version_creator']);
111 1
            $contentCreateStruct->ownerId = $versionCreator->id;
112
        }
113
114 11
        if (isset($step->dsl['modification_date'])) {
115 1
            $contentCreateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']);
116
        }
117
118
        // instantiate a location create struct from the parent location:
119
        // BC
120 11
        $locationId = isset($step->dsl['parent_location']) ? $step->dsl['parent_location'] : (
121 11
            isset($step->dsl['main_location']) ? $step->dsl['main_location'] : null
122
        );
123
        // 1st resolve references
124 11
        $locationId = $this->referenceResolver->resolveReference($locationId);
125
        // 2nd allow to specify the location via remote_id
126 11
        $locationId = $this->locationManager->matchLocationByKey($locationId)->id;
127 11
        $locationCreateStruct = $locationService->newLocationCreateStruct($locationId);
128
129 11
        if (isset($step->dsl['location_remote_id'])) {
130 2
            $locationCreateStruct->remoteId = $step->dsl['location_remote_id'];
131
        }
132
133 11
        if (isset($step->dsl['priority'])) {
134 1
            $locationCreateStruct->priority = $step->dsl['priority'];
135
        }
136
137 11
        if (isset($step->dsl['is_hidden'])) {
138 1
            $locationCreateStruct->hidden = $step->dsl['is_hidden'];
139
        }
140
141 11
        if (isset($step->dsl['sort_field'])) {
142 1
            $locationCreateStruct->sortField = $this->sortConverter->hash2SortField($step->dsl['sort_field']);
143
        } else {
144 11
            $locationCreateStruct->sortField = $contentType->defaultSortField;
145
        }
146
147 11
        if (isset($step->dsl['sort_order'])) {
148 1
            $locationCreateStruct->sortOrder = $this->sortConverter->hash2SortOrder($step->dsl['sort_order']);
149
        } else {
150 11
            $locationCreateStruct->sortOrder = $contentType->defaultSortOrder;
151
        }
152
153 11
        $locations = array($locationCreateStruct);
154
155
        // BC
156 11
        $other_locations = isset($step->dsl['other_parent_locations']) ? $step->dsl['other_parent_locations'] : (
157 11
            isset($step->dsl['other_locations']) ? $step->dsl['other_locations'] : null
158
        );
159 11
        if (isset($other_locations)) {
160
            foreach ($other_locations as $locationId) {
161
                $locationId = $this->referenceResolver->resolveReference($locationId);
162
                $locationId = $this->locationManager->matchLocationByKey($locationId)->id;
163
                $secondaryLocationCreateStruct = $locationService->newLocationCreateStruct($locationId);
164
                array_push($locations, $secondaryLocationCreateStruct);
165
            }
166
        }
167
168
        // create a draft using the content and location create struct and publish it
169 11
        $draft = $contentService->createContent($contentCreateStruct, $locations);
170 10
        $content = $contentService->publishVersion($draft->versionInfo);
171
172 10
        if (isset($step->dsl['object_states'])) {
173 2
            $this->setObjectStates($content, $step->dsl['object_states']);
174
        }
175
176
        // 2nd part of the hack: re-set the content owner to its intended value
177 10
        if (isset($step->dsl['version_creator']) || isset($step->dsl['publication_date'])) {
178 1
            $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
179
180 1
            if (isset($step->dsl['version_creator'])) {
181 1
                $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
            }
183 1
            if (isset($step->dsl['publication_date'])) {
184 1
                $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 1
            if (isset($step->dsl['modification_date'])) {
188
                $contentMetaDataUpdateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']);
189
            }
190
191 1
            $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct);
192
        }
193
194 10
        $this->setReferences($content, $step);
0 ignored issues
show
Documentation introduced by
$content is of type object<eZ\Publish\API\Re...Values\Content\Content>, but the function expects a object<Object>|object<Ka...ion\AbstractCollection>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
195
196 10
        return $content;
197
    }
198
199 6
    protected function load($step)
200
    {
201 6
        $contentCollection = $this->matchContents('load', $step);
202
203 6
        $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...ecutor::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
205 5
        return $contentCollection;
206
    }
207
208
    /**
209
     * Handles the content update migration action type
210
     *
211
     * @todo handle updating of more metadata fields
212
     */
213 9
    protected function update($step)
214
    {
215 9
        $contentService = $this->repository->getContentService();
216 9
        $contentTypeService = $this->repository->getContentTypeService();
217
218 9
        $contentCollection = $this->matchContents('update', $step);
219
220 9
        if (count($contentCollection) > 1 && isset($step->dsl['references'])) {
221
            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 9
        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 9
        $contentType = array();
229
230 9
        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 9
            $contentInfo = $content->contentInfo;
232
233 9
            if (!isset($contentType[$contentInfo->contentTypeId])) {
234 9
                $contentType[$contentInfo->contentTypeId] = $contentTypeService->loadContentType($contentInfo->contentTypeId);
235
            }
236
237 9
            if (isset($step->dsl['attributes']) || isset($step->dsl['version_creator'])) {
238 4
                $contentUpdateStruct = $contentService->newContentUpdateStruct();
239
240 4
                if (isset($step->dsl['attributes'])) {
241 4
                    $this->setFields($contentUpdateStruct, $step->dsl['attributes'], $contentType[$contentInfo->contentTypeId], $step);
242
                }
243
244 4
                $versionCreator = null;
245 4
                if (isset($step->dsl['version_creator'])) {
246 1
                    $versionCreator = $this->getUser($step->dsl['version_creator']);
247
                }
248
249 4
                $draft = $contentService->createContentDraft($contentInfo, null, $versionCreator);
250 4
                $contentService->updateContent($draft->versionInfo, $contentUpdateStruct);
251 4
                $content = $contentService->publishVersion($draft->versionInfo);
252
            }
253
254 9
            if (isset($step->dsl['always_available']) ||
255 9
                isset($step->dsl['new_remote_id']) ||
256 8
                isset($step->dsl['owner']) ||
257 7
                isset($step->dsl['modification_date']) ||
258 9
                isset($step->dsl['publication_date'])) {
259
260 2
                $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
261
262 2
                if (isset($step->dsl['always_available'])) {
263
                    $contentMetaDataUpdateStruct->alwaysAvailable = $step->dsl['always_available'];
264
                }
265
266 2
                if (isset($step->dsl['new_remote_id'])) {
267 1
                    $contentMetaDataUpdateStruct->remoteId = $step->dsl['new_remote_id'];
268
                }
269
270 2 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 2
                    $owner = $this->getUser($step->dsl['owner']);
272 2
                    $contentMetaDataUpdateStruct->ownerId = $owner->id;
273
                }
274
275 2
                if (isset($step->dsl['modification_date'])) {
276 1
                    $contentMetaDataUpdateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']);
277
                }
278
279 2
                if (isset($step->dsl['publication_date'])) {
280 1
                    $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($step->dsl['publication_date']);
281
                }
282
283 2
                $content = $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct);
284
            }
285
286 9
            if (isset($step->dsl['section'])) {
287 1
                $this->setSection($content, $step->dsl['section']);
288
            }
289
290 9
            if (isset($step->dsl['object_states'])) {
291 1
                $this->setObjectStates($content, $step->dsl['object_states']);
292
            }
293
294 9
            if (isset($step->dsl['main_location'])) {
295 1
                $this->setMainLocation($content, $step->dsl['main_location']);
296
297
            }
298 9
            $contentCollection[$key] = $content;
299
        }
300
301 9
        $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...ecutor::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
303 9
        return $contentCollection;
304
    }
305
306
    /**
307
     * Handles the content delete migration action type
308
     */
309 9
    protected function delete($step)
310
    {
311 9
        $contentCollection = $this->matchContents('delete', $step);
312
313 9
        $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...ecutor::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 9
        $contentService = $this->repository->getContentService();
316
317 9
        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
            try {
319 9
                $contentService->deleteContent($content->contentInfo);
320 9
            } catch (NotFoundException $e) {
321
                // Someone else (or even us, by virtue of location tree?) removed the content which we found just a
322
                // second ago. We can safely ignore this
323
            }
324
        }
325
326 9
        return $contentCollection;
327
    }
328
329
    /**
330
     * @param string $action
331
     * @return ContentCollection
332
     * @throws \Exception
333
     */
334 13 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
    {
336 13
        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
        }
339
340
        // Backwards compat
341
342 13
        if (isset($step->dsl['match'])) {
343 13
            $match = $step->dsl['match'];
344
        } else {
345 1
            if (isset($step->dsl['object_id'])) {
346 1
                $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 13
        $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...
354
355 13
        return $this->contentMatcher->match($match);
356
    }
357
358
    /**
359
     * @param Content $content
360
     * @param array $references the definitions of the references to set
361
     * @throws \InvalidArgumentException When trying to assign a reference to an unsupported attribute
362
     * @return array key: the reference names, values: the reference values
363
     */
364 9
    protected function getReferencesValues($content, array $references, $step)
365
    {
366 9
        $refs = array();
367
368 9
        foreach ($references as $reference) {
369
370 9
            switch ($reference['attribute']) {
371 9
                case 'object_id':
372 9
                case 'content_id':
373 9
                case 'id':
374 9
                    $value = $content->id;
375 9
                    break;
376 7
                case 'remote_id':
377 7
                case 'content_remote_id':
378 2
                    $value = $content->contentInfo->remoteId;
379 2
                    break;
380 7
                case 'always_available':
381 1
                    $value = $content->contentInfo->alwaysAvailable;
382 1
                    break;
383 7
                case 'content_type_id':
384 1
                    $value = $content->contentInfo->contentTypeId;
385 1
                    break;
386 7
                case 'content_type_identifier':
387 1
                    $contentTypeService = $this->repository->getContentTypeService();
388 1
                    $value = $contentTypeService->loadContentType($content->contentInfo->contentTypeId)->identifier;
389 1
                    break;
390 7
                case 'current_version':
391 7
                case 'current_version_no':
392 1
                    $value = $content->contentInfo->currentVersionNo;
393 1
                    break;
394 7
                case 'location_id':
395 5
                case 'main_location_id':
396 3
                    $value = $content->contentInfo->mainLocationId;
397 3
                    break;
398 5
                case 'main_language_code':
399 1
                    $value = $content->contentInfo->mainLanguageCode;
400 1
                    break;
401 5
                case 'modification_date':
402 1
                    $value = $content->contentInfo->modificationDate->getTimestamp();
403 1
                    break;
404 5
                case 'name':
405 1
                    $value = $content->contentInfo->name;
406 1
                    break;
407 5
                case 'owner_id':
408 1
                    $value = $content->contentInfo->ownerId;
409 1
                    break;
410 5
                case 'path':
411 2
                    $locationService = $this->repository->getLocationService();
412 2
                    $value = $locationService->loadLocation($content->contentInfo->mainLocationId)->pathString;
413 2
                    break;
414 4
                case 'publication_date':
415 1
                    $value = $content->contentInfo->publishedDate->getTimestamp();
416 1
                    break;
417 4
                case 'section_id':
418 1
                    $value = $content->contentInfo->sectionId;
419 1
                    break;
420 4
                case 'section_identifier':
421 1
                    $sectionService = $this->repository->getSectionService();
422 1
                    $value = $sectionService->loadSection($content->contentInfo->sectionId)->identifier;
423 1
                    break;
424 3
                case 'version_count':
425 1
                    $contentService = $this->repository->getContentService();
426 1
                    $value = count($contentService->loadVersions($content->contentInfo));
427 1
                    break;
428
                default:
429 2
                    if (strpos($reference['attribute'], 'object_state.') === 0) {
430 1
                        $stateGroupKey = substr($reference['attribute'], 13);
431 1
                        $stateGroup = $this->objectStateGroupMatcher->matchOneByKey($stateGroupKey);
432 1
                        $value = $stateGroupKey . '/' . $this->repository->getObjectStateService()->
433 1
                            getContentState($content->contentInfo, $stateGroup)->identifier;
434 1
                        break;
435
                    }
436
437
                    // allow to get the value of fields as well as their sub-parts
438 1
                    if (strpos($reference['attribute'], 'attributes.') === 0) {
439 1
                        $contentType = $this->repository->getContentTypeService()->loadContentType(
440 1
                            $content->contentInfo->contentTypeId
441
                        );
442 1
                        $parts = explode('.', $reference['attribute']);
443
                        // totally not sure if this list of special chars is correct for what could follow a jmespath identifier...
444
                        // also what about quoted strings?
445 1
                        $fieldIdentifier = preg_replace('/[[(|&!{].*$/', '', $parts[1]);
446 1
                        $field = $content->getField($fieldIdentifier);
0 ignored issues
show
Bug introduced by
The method getField() does not exist on eZ\Publish\API\Repository\Values\Content\Content. Did you maybe mean getFields()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
447 1
                        $fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier);
448 1
                        $hashValue = $this->fieldHandlerManager->fieldValueToHash(
449 1
                            $fieldDefinition->fieldTypeIdentifier, $contentType->identifier, $field->value
450
                        );
451 1
                        if (is_array($hashValue)) {
452 1 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...
453
                                throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute'] . ': the given attribute has an array value');
454
                            }
455 1
                            $value = JmesPath::search(implode('.', array_slice($parts, 1)), array($fieldIdentifier => $hashValue));
456
                        } else {
457
                            if (count($parts) > 2) {
458
                                throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute'] . ': the given attribute has a scalar value');
459
                            }
460
                            $value = $hashValue;
461
                        }
462 1
                        break;
463
                    }
464
465
                    throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute']);
466
            }
467
468 9
            $refs[$reference['identifier']] = $value;
469
        }
470
471 9
        return $refs;
472
    }
473
474
    /**
475
     * @param array $matchCondition
476
     * @param string $mode
477
     * @param array $context
478
     * @throws \Exception
479
     * @return array
480
     *
481
     * @todo add support for dumping all object languages
482
     * @todo add 2ndary locations when in 'update' mode
483
     * @todo add dumping of sort_field and sort_order for 2ndary locations
484
     */
485 3
    public function generateMigration(array $matchCondition, $mode, array $context = array())
486
    {
487 3
        $previousUserId = $this->loginUser($this->getAdminUserIdentifierFromContext($context));
488 3
        $contentCollection = $this->contentMatcher->match($matchCondition);
489 3
        $data = array();
490
491
        /** @var \eZ\Publish\API\Repository\Values\Content\Content $content */
492 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...
493
494 3
            $location = $this->repository->getLocationService()->loadLocation($content->contentInfo->mainLocationId);
495 3
            $contentType = $this->repository->getContentTypeService()->loadContentType(
496 3
                $content->contentInfo->contentTypeId
497
            );
498
499
            $contentData = array(
500 3
                'type' => reset($this->supportedStepTypes),
501 3
                'mode' => $mode
502
            );
503
504
            switch ($mode) {
505 3
                case 'create':
506 1
                    $contentData = array_merge(
507 1
                        $contentData,
508
                        array(
509 1
                            'content_type' => $contentType->identifier,
510 1
                            'parent_location' => $location->parentLocationId,
511 1
                            'priority' => $location->priority,
512 1
                            'is_hidden' => $location->invisible,
513 1
                            'sort_field' => $this->sortConverter->sortField2Hash($location->sortField),
514 1
                            'sort_order' => $this->sortConverter->sortOrder2Hash($location->sortOrder),
515 1
                            'remote_id' => $content->contentInfo->remoteId,
516 1
                            'location_remote_id' => $location->remoteId
517
                        )
518
                    );
519 1
                    $locationService = $this->repository->getLocationService();
520 1
                    $locations = $locationService->loadLocations($content->contentInfo);
521 1
                    if (count($locations) > 1) {
522
                        $otherParentLocations = array();
523
                        foreach($locations as $otherLocation) {
524
                            if ($otherLocation->id != $location->id) {
525
                                $otherParentLocations[] = $otherLocation->parentLocationId;
526
                            }
527
                        }
528
                        $contentData['other_parent_locations'] = $otherParentLocations;
529
                    }
530 1
                    break;
531 2 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...
532 1
                    $contentData = array_merge(
533 1
                        $contentData,
534
                        array(
535
                            'match' => array(
536 1
                                ContentMatcher::MATCH_CONTENT_REMOTE_ID => $content->contentInfo->remoteId
537
                            ),
538 1
                            'new_remote_id' => $content->contentInfo->remoteId,
539
                        )
540
                    );
541 1
                    break;
542 1 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...
543 1
                    $contentData = array_merge(
544 1
                        $contentData,
545
                        array(
546
                            'match' => array(
547 1
                                ContentMatcher::MATCH_CONTENT_REMOTE_ID => $content->contentInfo->remoteId
548
                            )
549
                        )
550
                    );
551 1
                    break;
552
                default:
553
                    throw new \Exception("Executor 'content' doesn't support mode '$mode'");
554
            }
555
556 3
            if ($mode != 'delete') {
557
558 2
                $attributes = array();
559 2
                foreach ($content->getFieldsByLanguage($this->getLanguageCodeFromContext($context)) as $fieldIdentifier => $field) {
560 2
                    $fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier);
561 2
                    $attributes[$field->fieldDefIdentifier] = $this->fieldHandlerManager->fieldValueToHash(
562 2
                        $fieldDefinition->fieldTypeIdentifier, $contentType->identifier, $field->value
563
                    );
564
                }
565
566 2
                $contentData = array_merge(
567 2
                    $contentData,
568
                    array(
569 2
                        'lang' => $this->getLanguageCodeFromContext($context),
570 2
                        'section' => $content->contentInfo->sectionId,
571 2
                        'owner' => $content->contentInfo->ownerId,
572 2
                        'modification_date' => $content->contentInfo->modificationDate->getTimestamp(),
573 2
                        'publication_date' => $content->contentInfo->publishedDate->getTimestamp(),
574 2
                        'always_available' => (bool)$content->contentInfo->alwaysAvailable,
575 2
                        'attributes' => $attributes
576
                    )
577
                );
578
            }
579
580 3
            $data[] = $contentData;
581
        }
582
583 3
        $this->loginUser($previousUserId);
584 3
        return $data;
585
    }
586
587
    /**
588
     * Helper function to set the fields of a ContentCreateStruct based on the DSL attribute settings.
589
     *
590
     * @param ContentCreateStruct|ContentUpdateStruct $createOrUpdateStruct
591
     * @param array $fields see description of expected format in code below
592
     * @param ContentType $contentType
593
     * @param $step
594
     * @throws \Exception
595
     */
596 11
    protected function setFields($createOrUpdateStruct, array $fields, ContentType $contentType, $step)
597
    {
598 11
        $fields = $this->normalizeFieldDefs($fields, $step);
599
600
        if ($this->hasLanguageCodesAsKeys($fields)) {
601 11
            $fieldsList = $this->parseMultiLangFields($fields);
0 ignored issues
show
Bug introduced by
The method parseMultiLangFields() does not seem to exist on object<Kaliop\eZMigratio...xecutor\ContentManager>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
602
        } else {
603 11
            $fieldsList = $this->parseSingleLangFields($fields, $this->getLanguageCode($step));
0 ignored issues
show
Bug introduced by
The method parseSingleLangFields() does not seem to exist on object<Kaliop\eZMigratio...xecutor\ContentManager>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
604
        }
605
606 9
        foreach ($fieldsList as $fieldIdentifier => $fieldLanguages) {
607 9
            foreach ($fieldLanguages as $language => $fieldValue) {
608 9
                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...
609
                    throw new \Exception("Field '$fieldIdentifier' is not present in content type '{$contentType->identifier}'");
610 3
                }
611 3
612
                $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...
613
                $fieldValue = $this->getFieldValue($fieldValue, $fieldDefinition, $contentType->identifier, $step->context);
614 11
                $createOrUpdateStruct->setField($fieldIdentifier, $fieldValue, $language);
615
            }
616
        }
617
    }
618 11
619 11
    /**
620
     * Helper function to accommodate the definition of fields
621 11
     * - using a legacy DSL version
622
     * - using either single-language or multi-language style
623 11
     * 
624
     * @param array $fields
625 11
     * @return array
626
     */
627 1
    protected function normalizeFieldDefs($fields, $step)
628
    {
629 1
        $convertedFields = [];
630 1
        $i = 0;
631
        // the 'easy' yml: key = field name, value = value
632 1
        // deprecated: the 'legacy' yml: key = numerical index, value = array ( field name => value )
633 1
        foreach ($fields as $key => $field) {
634 1
            if ($key === $i && is_array($field) && count($field) == 1) {
635
                // each $field is one key value pair
636 2
                // eg.: $field = array($fieldIdentifier => $fieldValue)
637
                reset($field);
638 2
                $fieldIdentifier = key($field);
639 2
                $fieldValue = $field[$fieldIdentifier];
640
641 2
                $convertedFields[$fieldIdentifier] = $fieldValue;
642
            } else {
643 2
                $convertedFields[$key] = $field;
644 2
            }
645
            $i++;
646 2
        }
647
648 1
        // transform single-language field defs in multilang ones
649
        if (!$this->hasLanguageCodesAsKeys($convertedFields)) {
650 1
            $language = $this->getLanguageCode($step);
651 1
652 1
            foreach ($convertedFields as $fieldIdentifier => $fieldValue) {
653
                $convertedFields[$fieldIdentifier] = array($language => $fieldValue);
654
            }
655
        }
656
657 1
        return $convertedFields;
658
    }
659
660
    /**
661 1
     * Checks whether all fields are using multilang syntax ie. a valid language as key.
662 1
     *
663 1
     * @param array $fields
664 1
     * @return bool
665 1
     */
666
    protected function hasLanguageCodesAsKeys(array $fields)
667
    {
668
        $languageCodes = $this->getContentLanguageCodes();
669
670
        foreach ($fields as $fieldIdentifier => $fieldData) {
671
            if (!is_array($fieldData) || empty($fieldData)) {
672
                return false;
673
            }
674
675
            foreach ($fieldData as $key => $data) {
676
                if (!in_array($key, $languageCodes)) {
677 11
                    return false;
678
                }
679 11
            }
680
        }
681 11
682
        return true;
683 3
    }
684 1
685
    /**
686
     * Returns all enabled Languages in the repo.
687 3
     * @todo move to parent class?
688 3
     *
689 3
     * @return string[]
690
     */
691
    protected function getContentLanguageCodes()
692 11
    {
693
        return array_map(
694
            function($language) {
695
                return $language->languageCode;
696
            },
697
            array_filter(
698
                $this->repository->getContentLanguageService()->loadLanguages(),
699
                function ($language) {
700
                    return $language->enabled;
701
                }
702
            )
703
        );
704
    }
705 11
706 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...
707
    {
708
        $sectionKey = $this->referenceResolver->resolveReference($sectionKey);
709
        $section = $this->sectionMatcher->matchOneByKey($sectionKey);
710
711 11
        $sectionService = $this->repository->getSectionService();
712
        $sectionService->assignSection($content->contentInfo, $section);
713 11
    }
714
715
    protected function setObjectStates(Content $content, array $stateKeys)
716
    {
717
        foreach ($stateKeys as $stateKey) {
718
            $stateKey = $this->referenceResolver->resolveReference($stateKey);
719
            /** @var \eZ\Publish\API\Repository\Values\ObjectState\ObjectState $state */
720
            $state = $this->objectStateMatcher->matchOneByKey($stateKey);
721 2
722
            $stateService = $this->repository->getObjectStateService();
723 2
            $stateService->setContentState($content->contentInfo, $state->getObjectStateGroup(), $state);
724 2
        }
725
    }
726
727
    protected function setMainLocation(Content $content, $locationId)
728
    {
729
        $locationId = $this->referenceResolver->resolveReference($locationId);
730
        if (is_int($locationId) || ctype_digit($locationId)) {
731 1
            $location = $this->repository->getLocationService()->loadLocation($locationId);
732
        } else {
733 1
            $location = $this->repository->getLocationService()->loadLocationByRemoteId($locationId);
734
        }
735
736 1
        if ($location->contentInfo->id != $content->id) {
737
            throw new \Exception("Can not set main location {$location->id} to content {$content->id} as it belongs to another object");
738
        }
739
740
        $contentService = $this->repository->getContentService();
741
        $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
742
        $contentMetaDataUpdateStruct->mainLocationId = $location->id;
743
        $contentService->updateContentMetadata($location->contentInfo, $contentMetaDataUpdateStruct);
744
    }
745
746
    /**
747
     * Create the field value from the migration definition hash
748
     *
749
     * @param mixed $value
750
     * @param FieldDefinition $fieldDefinition
751
     * @param string $contentTypeIdentifier
752
     * @param array $context
753
     * @throws \InvalidArgumentException
754
     * @return mixed
755
     */
756
    protected function getFieldValue($value, FieldDefinition $fieldDefinition, $contentTypeIdentifier, array $context = array())
757
    {
758
        $fieldTypeIdentifier = $fieldDefinition->fieldTypeIdentifier;
759
760
        if (is_array($value) || $this->fieldHandlerManager->managesField($fieldTypeIdentifier, $contentTypeIdentifier)) {
761
            // since we now allow refs to be arrays, let's attempt a 1st pass at resolving them here instead of every single fieldHandler...
762
            if (is_string($value) && $this->fieldHandlerManager->doPreResolveStringReferences($fieldTypeIdentifier, $contentTypeIdentifier)) {
763
                $value = $this->referenceResolver->resolveReference($value);
764
            }
765
            // inject info about the current content type and field into the context
766
            $context['contentTypeIdentifier'] = $contentTypeIdentifier;
767
            $context['fieldIdentifier'] = $fieldDefinition->identifier;
768
            return $this->fieldHandlerManager->hashToFieldValue($fieldTypeIdentifier, $contentTypeIdentifier, $value, $context);
769
        }
770
771
        return $this->getSingleFieldValue($value, $fieldDefinition, $contentTypeIdentifier, $context);
772
    }
773
774
    /**
775
     * Create the field value for a primitive field from the migration definition hash
776
     *
777
     * @param mixed $value
778
     * @param FieldDefinition $fieldDefinition
779
     * @param string $contentTypeIdentifier
780
     * @param array $context
781
     * @throws \InvalidArgumentException
782
     * @return mixed
783
     */
784
    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...
785
    {
786
        // booleans were handled here. They are now handled as complextypes
787
788
        // q: do we really want this to happen by default on all scalar field values?
789
        // Note: if you want this *not* to happen, register a complex field for your scalar field...
790
        $value = $this->referenceResolver->resolveReference($value);
791
792
        return $value;
793
    }
794
795
    /**
796
     * Load user using either login, email, id - resolving eventual references
797
     * @param int|string $userKey
798
     * @return \eZ\Publish\API\Repository\Values\User\User
799
     */
800
    protected function getUser($userKey)
801
    {
802
        $userKey = $this->referenceResolver->resolveReference($userKey);
803
        return $this->userMatcher->matchOneByKey($userKey);
804
    }
805
806
    /**
807
     * @param int|string $date if integer, we assume a timestamp
808
     * @return \DateTime
809
     */
810
    protected function toDateTime($date)
811
    {
812
        if (is_int($date)) {
813
            return new \DateTime("@" . $date);
814
        } else {
815
            return new \DateTime($date);
816
        }
817
    }
818
}
819