Completed
Push — master ( a0b010...baae4d )
by Gaetano
10:41
created

ContentManager::generateMigration()   D

Complexity

Conditions 10
Paths 10

Size

Total Lines 101
Code Lines 69

Duplication

Lines 21
Ratio 20.79 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
dl 21
loc 101
ccs 0
cts 0
cp 0
rs 4.8196
c 0
b 0
f 0
cc 10
eloc 69
nc 10
nop 2
crap 110

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

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

Loading history...
93
            $owner = $this->getUser($this->dsl['owner']);
94 1
            $contentCreateStruct->ownerId = $owner->id;
95
        }
96 1
97 1
        // This is a bit tricky, as the eZPublish API does not support having a different creator and owner with only 1 version.
98
        // We allow it, hoping that nothing gets broken because of it
99
        if (isset($this->dsl['version_creator'])) {
100
            $realContentOwnerId = $contentCreateStruct->ownerId;
101
            if ($realContentOwnerId == null) {
102
                $realContentOwnerId = $this->repository->getCurrentUser()->id;
103
            }
104
            $versionCreator = $this->getUser($this->dsl['version_creator']);
105
            $contentCreateStruct->ownerId = $versionCreator->id;
106
        }
107
108
        if (isset($this->dsl['modification_date'])) {
109
            $contentCreateStruct->modificationDate = $this->toDateTime($this->dsl['modification_date']);
110
        }
111
112
        // instantiate a location create struct from the parent location:
113
        // BC
114
        $locationId = isset($this->dsl['parent_location']) ? $this->dsl['parent_location'] : (
115
            isset($this->dsl['main_location']) ? $this->dsl['main_location'] : null
116
        );
117
        // 1st resolve references
118
        $locationId = $this->referenceResolver->resolveReference($locationId);
119
        // 2nd allow to specify the location via remote_id
120
        $locationId = $this->locationManager->matchLocationByKey($locationId)->id;
121 1
        $locationCreateStruct = $locationService->newLocationCreateStruct($locationId);
122
123 1
        if (isset($this->dsl['location_remote_id'])) {
124
            $locationCreateStruct->remoteId = $this->dsl['location_remote_id'];
125
        }
126
127 1
        if (isset($this->dsl['priority'])) {
128
            $locationCreateStruct->priority = $this->dsl['priority'];
129 1
        }
130 1
131
        if (isset($this->dsl['is_hidden'])) {
132 1
            $locationCreateStruct->hidden = $this->dsl['is_hidden'];
133 1
        }
134 1
135
        if (isset($this->dsl['sort_field'])) {
136 1
            $locationCreateStruct->sortField = $this->sortConverter->hash2SortField($this->dsl['sort_field']);
137
        } else {
138 1
            $locationCreateStruct->sortField = $contentType->defaultSortField;
139 1
        }
140 1
141
        if (isset($this->dsl['sort_order'])) {
142 1
            $locationCreateStruct->sortOrder = $this->sortConverter->hash2SortOrder($this->dsl['sort_order']);
143 1
        } else {
144 1
            $locationCreateStruct->sortOrder = $contentType->defaultSortOrder;
145
        }
146 1
147
        $locations = array($locationCreateStruct);
148
149
        // BC
150
        $other_locations = isset($this->dsl['other_parent_locations']) ? $this->dsl['other_parent_locations'] : (
151
            isset($this->dsl['other_locations']) ? $this->dsl['other_locations'] : null
152
        );
153
        if (isset($other_locations)) {
154
            foreach ($other_locations as $locationId) {
155
                $locationId = $this->referenceResolver->resolveReference($locationId);
156
                $locationId = $this->locationManager->matchLocationByKey($locationId)->id;
157
                $secondaryLocationCreateStruct = $locationService->newLocationCreateStruct($locationId);
158
                array_push($locations, $secondaryLocationCreateStruct);
159
            }
160
        }
161 1
162 1
        // create a draft using the content and location create struct and publish it
163 1
        $draft = $contentService->createContent($contentCreateStruct, $locations);
164
        $content = $contentService->publishVersion($draft->versionInfo);
165
166
        if (isset($this->dsl['object_states'])) {
167
            $this->setObjectStates($content, $this->dsl['object_states']);
168 3
        }
169
170 3
        // 2nd part of the hack: re-set the content owner to its intended value
171
        if (isset($this->dsl['version_creator']) || isset($this->dsl['publication_date'])) {
172 3
            $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
173
174 3
            if (isset($this->dsl['version_creator'])) {
175
                $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...
176 3
            }
177 3
            if (isset($this->dsl['publication_date'])) {
178
                $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($this->dsl['publication_date']);
179
            }
180
181 3
            $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct);
182 3
        }
183
184
        $this->setReferences($content);
185
186
        return $content;
187
    }
188
189 3
    /**
190
     * Handle the content update migration action type
191 3
     *
192
     * @todo handle updating of more metadata fields
193
     */
194
    protected function update()
195
    {
196 3
        $contentService = $this->repository->getContentService();
197 1
        $contentTypeService = $this->repository->getContentTypeService();
198 1
199 1
        $contentCollection = $this->matchContents('update');
200
201
        if (count($contentCollection) > 1 && isset($this->dsl['references'])) {
202 1
            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");
203
        }
204 3
205
        $contentType = null;
206
207 3
        foreach ($contentCollection as $key => $content) {
0 ignored issues
show
Bug introduced by
The expression $contentCollection of type object<Kaliop\eZMigratio...ContentCollection>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
208 3
            $contentInfo = $content->contentInfo;
209 1
210 1
            if ($contentType == null) {
211 1
                $contentType = $contentTypeService->loadContentType($contentInfo->contentTypeId);
212 1
            }
213 1
214 1
            $contentUpdateStruct = $contentService->newContentUpdateStruct();
215 3
216 3
            if (isset($this->dsl['attributes'])) {
217 3
                $this->setFields($contentUpdateStruct, $this->dsl['attributes'], $contentType);
218
            }
219 3
220
            $versionCreator = null;
221 3
            if (isset($this->dsl['version_creator'])) {
222
                $versionCreator = $this->getUser($this->dsl['version_creator']);
223
            }
224
225
            $draft = $contentService->createContentDraft($contentInfo, null, $versionCreator);
226
            $contentService->updateContent($draft->versionInfo, $contentUpdateStruct);
227
            $content = $contentService->publishVersion($draft->versionInfo);
228
229
            if (isset($this->dsl['always_available']) ||
230
                isset($this->dsl['new_remote_id']) ||
231 4
                isset($this->dsl['owner']) ||
232
                isset($this->dsl['modification_date']) ||
233 4
                isset($this->dsl['publication_date'])) {
234
235
                $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
236 4
237
                if (isset($this->dsl['always_available'])) {
238 4
                    $contentMetaDataUpdateStruct->alwaysAvailable = $this->dsl['always_available'];
239
                }
240 4
241
                if (isset($this->dsl['new_remote_id'])) {
242 1
                    $contentMetaDataUpdateStruct->remoteId = $this->dsl['new_remote_id'];
243 1
                }
244
245 4 View Code Duplication
                if (isset($this->dsl['owner'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
246 3
                    $owner = $this->getUser($this->dsl['owner']);
247
                    $contentMetaDataUpdateStruct->ownerId = $owner->id;
248 4
                }
249 4
250 4
                if (isset($this->dsl['modification_date'])) {
251
                    $contentMetaDataUpdateStruct->modificationDate = $this->toDateTime($this->dsl['modification_date']);
252
                }
253
254
                if (isset($this->dsl['publication_date'])) {
255
                    $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($this->dsl['publication_date']);
256
                }
257
258
                $content = $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct);
259
            }
260
261
            if (isset($this->dsl['section'])) {
262 4
                $this->setSection($content, $this->dsl['section']);
263
            }
264 4
265 4
            if (isset($this->dsl['object_states'])) {
266
                $this->setObjectStates($content, $this->dsl['object_states']);
267
            }
268 4
269 4
            $contentCollection[$key] = $content;
270 4
        }
271
272 4
        $this->setReferences($contentCollection);
0 ignored issues
show
Bug introduced by
It seems like $contentCollection defined by $this->matchContents('update') on line 199 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...
273 1
274 1
        return $contentCollection;
275
    }
276 4
277
    /**
278
     * Handle the content delete migration action type
279
     */
280
    protected function delete()
281
    {
282
        $contentService = $this->repository->getContentService();
283
284
        $contentCollection = $this->matchContents('delete');
285
286
        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...
287 1
            try {
288
                $contentService->deleteContent($content->contentInfo);
289 1
            } catch (NotFoundException $e) {
290 1
                // Someone else (or even us, by virtue of location tree?) removed the content which we found just a
291
                // second ago. We can safely ignore this
292
            }
293
        }
294
295
        return $contentCollection;
296
    }
297
298
    /**
299
     * @param string $action
300
     * @return ContentCollection
301
     * @throws \Exception
302 3
     */
303 View Code Duplication
    protected function matchContents($action)
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...
304 3
    {
305 2
        if (!isset($this->dsl['object_id']) && !isset($this->dsl['remote_id']) && !isset($this->dsl['match'])) {
306
            throw new \Exception("The ID or remote ID of an object or a Match Condition is required to $action a new location.");
307
        }
308 3
309
        // Backwards compat
310
        if (!isset($this->dsl['match'])) {
311
            if (isset($this->dsl['object_id'])) {
312
                $this->dsl['match'] = array('content_id' => $this->dsl['object_id']);
313
            } elseif (isset($this->dsl['remote_id'])) {
314
                $this->dsl['match'] = array('content_remote_id' => $this->dsl['remote_id']);
315 3
            }
316
        }
317 3
318 3
        $match = $this->dsl['match'];
319 3
320 3
        // convert the references passed in the match
321 3
        foreach ($match as $condition => $values) {
322 3
            if (is_array($values)) {
323 2
                foreach ($values as $position => $value) {
324 2
                    $match[$condition][$position] = $this->referenceResolver->resolveReference($value);
325 2
                }
326 1
            } else {
327 1
                $match[$condition] = $this->referenceResolver->resolveReference($values);
328 1
            }
329 1
        }
330
331
        return $this->contentMatcher->match($match);
332
    }
333 3
334
    /**
335 3
     * Helper function to set the fields of a ContentCreateStruct based on the DSL attribute settings.
336 3
     *
337
     * @param ContentCreateStruct|ContentUpdateStruct $createOrUpdateStruct
338 3
     * @param ContentType $contentType
339
     * @param array $fields see description of expected format in code below
340
     * @throws \Exception
341
     */
342
    protected function setFields($createOrUpdateStruct, array $fields, ContentType $contentType)
343
    {
344
        $i = 0;
345
        // the 'easy' yml: key = field name, value = value
346
        // deprecated: the 'legacy' yml: key = numerical index, value = array ( field name => value )
347
        foreach ($fields as $key => $field) {
348
349
            if ($key === $i && is_array($field) && count($field) == 1) {
350
                // each $field is one key value pair
351
                // 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...
352
                reset($field);
353
                $fieldIdentifier = key($field);
354
                $fieldValue = $field[$fieldIdentifier];
355
            } else {
356
                $fieldIdentifier = $key;
357
                $fieldValue = $field;
358
            }
359
360
            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...
361
                throw new \Exception("Field '$fieldIdentifier' is not present in field type '{$contentType->identifier}'");
362
            }
363
364
            $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...
365
            $fieldValue = $this->getFieldValue($fieldValue, $fieldDefinition, $contentType->identifier, $this->context);
366
367
            $createOrUpdateStruct->setField($fieldIdentifier, $fieldValue, $this->getLanguageCode());
368
369
            $i++;
370
        }
371
    }
372
373 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...
374
    {
375
        $sectionKey = $this->referenceResolver->resolveReference($sectionKey);
376
        $section = $this->sectionMatcher->matchOneByKey($sectionKey);
377
378
        $sectionService = $this->repository->getSectionService();
379
        $sectionService->assignSection($content->contentInfo, $section);
380
    }
381
382
    protected function setObjectStates(Content $content, array $stateKeys)
383
    {
384
        foreach ($stateKeys as $stateKey) {
385
            $stateKey = $this->referenceResolver->resolveReference($stateKey);
386
            /** @var \eZ\Publish\API\Repository\Values\ObjectState\ObjectState $state */
387
            $state = $this->objectStateMatcher->matchOneByKey($stateKey);
388
389
            $stateService = $this->repository->getObjectStateService();
390
            $stateService->setContentState($content->contentInfo, $state->getObjectStateGroup(), $state);
391
        }
392
    }
393
394
    /**
395
     * Create the field value from the migration definition hash
396
     *
397
     * @param mixed $value
398
     * @param FieldDefinition $fieldDefinition
399
     * @param string $contentTypeIdentifier
400
     * @param array $context
401
     * @throws \InvalidArgumentException
402
     * @return mixed
403
     */
404
    protected function getFieldValue($value, FieldDefinition $fieldDefinition, $contentTypeIdentifier, array $context = array())
405
    {
406
        $fieldTypeIdentifier = $fieldDefinition->fieldTypeIdentifier;
407
        if (is_array($value) || $this->complexFieldManager->managesField($fieldTypeIdentifier, $contentTypeIdentifier)) {
408
            // inject info about the current content type and field into the context
409
            $context['contentTypeIdentifier'] = $contentTypeIdentifier;
410
            $context['fieldIdentifier'] = $fieldDefinition->identifier;
411
            return $this->complexFieldManager->hashToFieldValue($fieldTypeIdentifier, $contentTypeIdentifier, $value, $context);
412
        }
413
414
        return $this->getSingleFieldValue($value, $fieldDefinition, $contentTypeIdentifier, $context);
415
    }
416
417
    /**
418
     * Create the field value for a primitive field from the migration definition hash
419
     *
420
     * @param mixed $value
421
     * @param FieldDefinition $fieldDefinition
422
     * @param string $contentTypeIdentifier
423
     * @param array $context
424
     * @throws \InvalidArgumentException
425
     * @return mixed
426
     */
427
    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...
428
    {
429
        // booleans were handled here. They are now handled as complextypes
430
431
        // q: do we really want this to happen by default on all scalar field values?
432
        // Note: if you want this *not* to happen, register a complex field for your scalar field...
433
        $value = $this->referenceResolver->resolveReference($value);
434
435
        return $value;
436
    }
437
438
    /**
439
     * Load user using either login, email, id - resolving eventual references
440
     * @param int|string $userKey
441
     * @return \eZ\Publish\API\Repository\Values\User\User
442
     */
443
    protected function getUser($userKey)
444
    {
445
        $userKey = $this->referenceResolver->resolveReference($userKey);
446
        return $this->userMatcher->matchOneByKey($userKey);
447
    }
448
449
    /**
450
     * Sets references to certain content attributes.
451
     *
452
     * @param \eZ\Publish\API\Repository\Values\Content\Content|ContentCollection $content
453
     * @throws \InvalidArgumentException When trying to set a reference to an unsupported attribute
454
     * @return boolean
455
     *
456
     * @todo add support for other attributes: contentTypeId, contentTypeIdentifier, section, etc... ?
457
     */
458
    protected function setReferences($content)
459
    {
460
        if (!array_key_exists('references', $this->dsl)) {
461
            return false;
462
        }
463
464
        if ($content instanceof ContentCollection) {
465
            if (count($content) > 1) {
466
                throw new \InvalidArgumentException('Content Manager does not support setting references for creating/updating of multiple contents');
467
            }
468
            $content = reset($content);
469
        }
470
471
        foreach ($this->dsl['references'] as $reference) {
472
473
            switch ($reference['attribute']) {
474
                case 'object_id':
475
                case 'content_id':
476
                case 'id':
477
                    $value = $content->id;
478
                    break;
479
                case 'remote_id':
480
                case 'content_remote_id':
481
                    $value = $content->contentInfo->remoteId;
482
                    break;
483
                case 'location_id':
484
                    $value = $content->contentInfo->mainLocationId;
485
                    break;
486
                case 'path':
487
                    $locationService = $this->repository->getLocationService();
488
                    $value = $locationService->loadLocation($content->contentInfo->mainLocationId)->pathString;
489
                    break;
490
                default:
491
                    throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute']);
492
            }
493
494
            $this->referenceResolver->addReference($reference['identifier'], $value);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Kaliop\eZMigrationBundle...erenceResolverInterface as the method addReference() does only exist in the following implementations of said interface: Kaliop\eZMigrationBundle...ver\ChainPrefixResolver, Kaliop\eZMigrationBundle...ver\ChainRegexpResolver, Kaliop\eZMigrationBundle...eResolver\ChainResolver, Kaliop\eZMigrationBundle...CustomReferenceResolver.

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
495
        }
496
497
        return true;
498
    }
499
500
    /**
501
     * @param int|string $date if integer, we assume a timestamp
502
     * @return \DateTime
503
     */
504
    protected function toDateTime($date)
505
    {
506
        if (is_int($date)) {
507
            return new \DateTime("@" . $date);
508
        } else {
509
            return new \DateTime($date);
510
        }
511
    }
512
513
    /**
514
     * @param array $matchCondition
515
     * @param string $mode
516
     * @throws \Exception
517
     * @return array
518
     *
519
     * @todo add support for dumping all object languages
520
     * @todo add 2ndary locations when in 'update' mode
521
     * @todo add dumping of sort_field and sort_order for 2ndary locations
522
     */
523
    public function generateMigration(array $matchCondition, $mode)
524
    {
525
        $previousUserId = $this->loginUser(self::ADMIN_USER_ID);
526
        $contentCollection = $this->contentMatcher->match($matchCondition);
527
        $data = array();
528
529
        /** @var \eZ\Publish\API\Repository\Values\Content\Content $content */
530
        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...
531
532
            $location = $this->repository->getLocationService()->loadLocation($content->contentInfo->mainLocationId);
533
            $contentType = $this->repository->getContentTypeService()->loadContentType(
534
                $content->contentInfo->contentTypeId
535
            );
536
537
            $contentData = array(
538
                'type' => reset($this->supportedStepTypes),
539
                'mode' => $mode
540
            );
541
542
            switch ($mode) {
543
                case 'create':
544
                    $contentData = array_merge(
545
                        $contentData,
546
                        array(
547
                            'content_type' => $contentType->identifier,
548
                            'parent_location' => $location->parentLocationId,
549
                            'priority' => $location->priority,
550
                            'is_hidden' => $location->invisible,
551
                            'sort_field' => $this->sortConverter->sortField2Hash($location->sortField),
552
                            'sort_order' => $this->sortConverter->sortOrder2Hash($location->sortOrder),
553
                            'remote_id' => $content->contentInfo->remoteId,
554
                            'location_remote_id' => $location->remoteId
555
                        )
556
                    );
557
                    $locationService = $this->repository->getLocationService();
558
                    $locations = $locationService->loadLocations($content->contentInfo);
559
                    if (count($locations) > 1) {
560
                        $otherParentLocations = array();
561
                        foreach($locations as $otherLocation) {
562
                            if ($otherLocation->id != $location->id) {
563
                                $otherParentLocations[] = $otherLocation->parentLocationId;
564
                            }
565
                        }
566
                        $contentData['other_parent_locations'] = $otherParentLocations;
567
                    }
568
                    break;
569 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...
570
                    $contentData = array_merge(
571
                        $contentData,
572
                        array(
573
                            'match' => array(
574
                                ContentMatcher::MATCH_CONTENT_REMOTE_ID => $content->contentInfo->remoteId
575
                            ),
576
                            'new_remote_id' => $content->contentInfo->remoteId,
577
                        )
578
                    );
579
                    break;
580 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...
581
                    $contentData = array_merge(
582
                        $contentData,
583
                        array(
584
                            'match' => array(
585
                                ContentMatcher::MATCH_CONTENT_REMOTE_ID => $content->contentInfo->remoteId
586
                            )
587
                        )
588
                    );
589
                    break;
590
                default:
591
                    throw new \Exception("Executor 'content' doesn't support mode '$mode'");
592
            }
593
594
            if ($mode != 'delete') {
595
596
                $attributes = array();
597
                foreach ($content->getFieldsByLanguage($this->getLanguageCode()) as $fieldIdentifier => $field) {
598
                    $fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier);
599
                    $attributes[$field->fieldDefIdentifier] = $this->complexFieldManager->fieldValueToHash(
600
                        $fieldDefinition->fieldTypeIdentifier, $contentType->identifier, $field->value
601
                    );
602
                }
603
604
                $contentData = array_merge(
605
                    $contentData,
606
                    array(
607
                        'lang' => $this->getLanguageCode(),
608
                        'section' => $content->contentInfo->sectionId,
609
                        'owner' => $content->contentInfo->ownerId,
610
                        'modification_date' => $content->contentInfo->modificationDate->getTimestamp(),
611
                        'publication_date' => $content->contentInfo->publishedDate->getTimestamp(),
612
                        'always_available' => (bool)$content->contentInfo->alwaysAvailable,
613
                        'attributes' => $attributes
614
                    )
615
                );
616
            }
617
618
            $data[] = $contentData;
619
        }
620
621
        $this->loginUser($previousUserId);
622
        return $data;
623
    }
624
}
625