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

LocationManager::matchContents()   D

Complexity

Conditions 10
Paths 13

Size

Total Lines 30
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
dl 0
loc 30
c 0
b 0
f 0
rs 4.8196
ccs 0
cts 0
cp 0
cc 10
eloc 16
nc 13
nop 2
crap 110

How to fix   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\Content\Location;
6
use Kaliop\eZMigrationBundle\API\Collection\ContentCollection;
7
use Kaliop\eZMigrationBundle\API\Collection\LocationCollection;
8
use Kaliop\eZMigrationBundle\Core\Matcher\ContentMatcher;
9
use Kaliop\eZMigrationBundle\Core\Matcher\LocationMatcher;
10
use Kaliop\eZMigrationBundle\Core\Helper\SortConverter;
11
12
/**
13
 * Handles location migrations.
14
 */
15
class LocationManager extends RepositoryExecutor
16
{
17
    protected $supportedStepTypes = array('location');
18 20
    protected $supportedActions = array('create', 'load', 'update', 'delete');
19
20 20
    protected $contentMatcher;
21 20
    protected $locationMatcher;
22 20
    protected $sortConverter;
23
24
    public function __construct(ContentMatcher $contentMatcher, LocationMatcher $locationMatcher, SortConverter $sortConverter)
25
    {
26
        $this->contentMatcher = $contentMatcher;
27 1
        $this->locationMatcher = $locationMatcher;
28
        $this->sortConverter = $sortConverter;
29 1
    }
30
31 1
    /**
32
     * Method to handle the create operation of the migration instructions
33
     */
34 1
    protected function create($step)
35 1
    {
36 1
        $locationService = $this->repository->getLocationService();
37 1
38 1
        if (!isset($step->dsl['parent_location']) && !isset($step->dsl['parent_location_id'])) {
39 1
            throw new \Exception('Missing parent location id. This is required to create the new location.');
40 1
        }
41 1
42
        // support legacy tag: parent_location_id
43 1
        if (!isset($step->dsl['parent_location']) && isset($step->dsl['parent_location_id'])) {
44
            $parentLocationIds = $step->dsl['parent_location_id'];
45 1
        } else {
46 1
            $parentLocationIds = $step->dsl['parent_location'];
47 1
        }
48
49 1
        if (!is_array($parentLocationIds)) {
50 1
            $parentLocationIds = array($parentLocationIds);
51
        }
52 1
53
        if (isset($step->dsl['is_main']) && count($parentLocationIds) > 1) {
54 1
            throw new \Exception('Can not set more than one new location as main.');
55 1
        }
56 1
57
        // resolve references and remote ids
58 1
        foreach ($parentLocationIds as $id => $parentLocationId) {
59 1
            $parentLocationId = $this->referenceResolver->resolveReference($parentLocationId);
60
            $parentLocationIds[$id] = $this->matchLocationByKey($parentLocationId)->id;
61 1
        }
62 1
63 1
        $contentCollection = $this->matchContents('create', $step);
64
65 1
        $locations = null;
66 1
        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...
67
            $contentInfo = $content->contentInfo;
68
69
            foreach ($parentLocationIds as $parentLocationId) {
70
                $locationCreateStruct = $locationService->newLocationCreateStruct($parentLocationId);
71
72
                if (isset($step->dsl['is_hidden'])) {
73
                    $locationCreateStruct->hidden = $step->dsl['is_hidden'];
74
                }
75 1
76
                if (isset($step->dsl['priority'])) {
77 1
                    $locationCreateStruct->priority = $step->dsl['priority'];
78
                }
79 1
80
                if (isset($step->dsl['sort_order'])) {
81 1
                    $locationCreateStruct->sortOrder = $this->getSortOrder($step->dsl['sort_order']);
82
                }
83
84
                if (isset($step->dsl['sort_field'])) {
85 1
                    $locationCreateStruct->sortField = $this->getSortField($step->dsl['sort_field']);
86
                }
87
88
                $location = $locationService->createLocation($contentInfo, $locationCreateStruct);
89 1
90
                if (isset($step->dsl['is_main'])) {
91
                    $this->setMainLocation($location);
92
                }
93
94
                $locations[] = $location;
95
            }
96
        }
97
98 1
        $locationCollection = new LocationCollection($locations);
0 ignored issues
show
Documentation introduced by
$locations is of type null, but the function expects a array.

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...
99
100
        $this->setReferences($locationCollection, $step);
101 1
102 1
        return $locationCollection;
103 1
    }
104 1
105 1
    protected function load($step)
106 1
    {
107
        $locationCollection = $this->matchLocations('load', $step);
108 1
109 1
        $this->setReferences($locationCollection, $step);
0 ignored issues
show
Bug introduced by
It seems like $locationCollection defined by $this->matchLocations('load', $step) on line 107 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...
110 1
111
        return $locationCollection;
112 1
    }
113
114
    /**
115
     * Updates information for a location like priority, sort field and sort order.
116 1
     * Updates the visibility of the location when needed.
117 1
     * Can move a location and its children to a new parent location or swap two locations.
118 1
     *
119
     * @todo add support for flexible matchers
120 1
     */
121
    protected function update($step)
122
    {
123
        $locationService = $this->repository->getLocationService();
124 1
125 1
        $locationCollection = $this->matchLocations('update', $step);
126
127
        if (count($locationCollection) > 1 && isset($step->dsl['references'])) {
128 1
            throw new \Exception("Can not execute Location update because multiple locations match, and a references section is specified in the dsl. References can be set when only 1 location matches");
129 1
        }
130 1
131 1
        if (count($locationCollection) > 1 && isset($step->dsl['swap_with_location'])) {
132 1
            throw new \Exception("Can not execute Location update because multiple locations match, and a swap_with_location is specified in the dsl.");
133
        }
134 1
135
        // support legacy tag: parent_location_id
136
        if (isset($step->dsl['swap_with_location']) && (isset($step->dsl['parent_location']) || isset($step->dsl['parent_location_id']))) {
137 1
            throw new \Exception('Cannot move location to a new parent and swap location with another location at the same time.');
138
        }
139 1
140 1
        foreach ($locationCollection as $key => $location) {
0 ignored issues
show
Bug introduced by
The expression $locationCollection of type object<Kaliop\eZMigratio...ocationCollection>|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...
141 1
142 1
            if (isset($step->dsl['priority'])
143 1
                || isset($step->dsl['sort_field'])
144 1
                || isset($step->dsl['sort_order'])
145 1
                || isset($step->dsl['remote_id'])
146
            ) {
147
                $locationUpdateStruct = $locationService->newLocationUpdateStruct();
148
149
                if (isset($step->dsl['priority'])) {
150
                    $locationUpdateStruct->priority = $step->dsl['priority'];
151
                }
152
153
                if (isset($step->dsl['sort_field'])) {
154
                    $locationUpdateStruct->sortField = $this->getSortField($step->dsl['sort_field'], $location->sortField);
155
                }
156 1
157 1
                if (isset($step->dsl['sort_order'])) {
158 1
                    $locationUpdateStruct->sortOrder = $this->getSortOrder($step->dsl['sort_order'], $location->sortOrder);
159
                }
160
161
                if (isset($step->dsl['remote_id'])) {
162
                    $locationUpdateStruct->remoteId = $step->dsl['remote_id'];
163
                }
164
165
                $location = $locationService->updateLocation($location, $locationUpdateStruct);
166
            }
167
168
            // Check if visibility needs to be updated
169
            if (isset($step->dsl['is_hidden'])) {
170
                if ($step->dsl['is_hidden']) {
171
                    $location = $locationService->hideLocation($location);
172
                } else {
173
                    $location = $locationService->unhideLocation($location);
174
                }
175
            }
176
177
            // Move or swap location
178
            if (isset($step->dsl['parent_location']) || isset($step->dsl['parent_location_id'])) {
179
                // Move the location and all its children to a new parent
180
                $parentLocationId = isset($step->dsl['parent_location']) ? $step->dsl['parent_location'] : $step->dsl['parent_location_id'];
181
                $parentLocationId = $this->referenceResolver->resolveReference($parentLocationId);
182 1
183
                $newParentLocation = $locationService->loadLocation($parentLocationId);
184 1
185 1
                $locationService->moveSubtree($location, $newParentLocation);
186
            } elseif (isset($step->dsl['swap_with_location'])) {
187
                // Swap locations
188
                $swapLocationId = $step->dsl['swap_with_location'];
189 1
                $swapLocationId = $this->referenceResolver->resolveReference($swapLocationId);
190 1
191 1
                $locationToSwap = $this->matchLocationByKey($swapLocationId);
192
193 1
                $locationService->swapLocation($location, $locationToSwap);
194
            }
195
196 1
            // make the location the main one
197 1
            if (isset($step->dsl['is_main'])) {
198
                $this->setMainLocation($location);
199
            }
200
201
            $locationCollection[$key] = $location;
202
        }
203
204 1
        $this->setReferences($locationCollection, $step);
0 ignored issues
show
Bug introduced by
It seems like $locationCollection defined by $this->matchLocations('update', $step) on line 125 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...
205 1
206 1
        return $locationCollection;
207 1
    }
208 1
209
    /**
210 1
     * Delete locations
211
     *
212
     * @todo add support for flexible matchers
213
     */
214
    protected function delete($step)
215
    {
216
        $locationCollection = $this->matchLocations('delete', $step);
217
218
        $this->setReferences($locationCollection, $step);
0 ignored issues
show
Bug introduced by
It seems like $locationCollection defined by $this->matchLocations('delete', $step) on line 216 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...
219
220 1
        $locationService = $this->repository->getLocationService();
221
222 1
        foreach ($locationCollection as $location) {
0 ignored issues
show
Bug introduced by
The expression $locationCollection of type object<Kaliop\eZMigratio...ocationCollection>|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...
223
            $locationService->deleteLocation($location);
224
        }
225
226
        return $locationCollection;
227 1
    }
228
229
    /**
230
     * @param string $action
231
     * @return LocationCollection
232
     * @throws \Exception
233
     */
234 View Code Duplication
    protected function matchLocations($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...
235 1
    {
236
        if (!isset($step->dsl['location_id']) && !isset($step->dsl['match'])) {
237
            throw new \Exception("The id or a match condition is required to $action a location");
238 1
        }
239 1
240
        // Backwards compat
241
        if (isset($step->dsl['match'])) {
242
            $match = $step->dsl['match'];
243
        } else {
244
            $match = array('location_id' => $step->dsl['location_id']);
245
        }
246 1
247 1
        // convert the references passed in the match
248 1
        $match = $this->resolveReferencesRecursively($match);
0 ignored issues
show
Deprecated Code introduced by
The method Kaliop\eZMigrationBundle...ReferencesRecursively() has been deprecated with message: will be moved into the reference resolver classes

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

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

Loading history...
249
250 1
        return $this->locationMatcher->match($match);
251
    }
252 1
253
    /**
254
     * Sets references to object attributes
255 1
     *
256 1
     * The Location Manager currently supports setting references to location id.
257 1
     *
258
     * @throws \InvalidArgumentException When trying to set a reference to an unsupported attribute.
259 1
     * @param \eZ\Publish\API\Repository\Values\Content\Location|LocationCollection $location
260
     * @return boolean
261
     */
262
    protected function setReferences($location, $step)
263 1
    {
264 1
        if (!array_key_exists('references', $step->dsl)) {
265
            return false;
266 1
        }
267
268 1
        $references = $this->setReferencesCommon($location, $step->dsl['references']);
269 1
        $location = $this->insureSingleEntity($location, $references);
270
271 1
        foreach ($references as $reference) {
272
            switch ($reference['attribute']) {
273
                case 'location_id':
274
                case 'id':
275
                    $value = $location->id;
276
                    break;
277
                case 'remote_id':
278
                case 'location_remote_id':
279
                    $value = $location->remoteId;
280
                    break;
281
                case 'always_available':
282
                    $value = $location->contentInfo->alwaysAvailable;
283
                    break;
284 1
                case 'content_id':
285 1
                    $value = $location->contentId;
286 1
                    break;
287 1
                case 'content_type_id':
288 1
                    $value = $location->contentInfo->contentTypeId;
289 1
                    break;
290 View Code Duplication
                case 'content_type_identifier':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
291 1
                    $contentTypeService = $this->repository->getContentTypeService();
292 1
                    $value = $contentTypeService->loadContentType($location->contentInfo->contentTypeId)->identifier;
293 1
                    break;
294 1
                case 'current_version':
295 1
                case 'current_version_no':
296
                    $value = $location->contentInfo->currentVersionNo;
297 1
                    break;
298
                case 'depth':
299 1
                    $value = $location->depth;
300
                    break;
301
                case 'is_hidden':
302
                    $value = $location->hidden;
303
                    break;
304
                case 'main_location_id':
305
                    $value = $location->contentInfo->mainLocationId;
306
                    break;
307
                case 'main_language_code':
308
                    $value = $location->contentInfo->mainLanguageCode;
309
                    break;
310
                case 'modification_date':
311 1
                    $value = $location->contentInfo->modificationDate->getTimestamp();
312
                    break;
313 1
                case 'name':
314 1
                    $value = $location->contentInfo->name;
315
                    break;
316
                case 'owner_id':
317 1
                    $value = $location->contentInfo->ownerId;
318 1
                    break;
319
                case 'parent_location_id':
320
                    $value = $location->parentLocationId;
321 1
                    break;
322 1
                case 'path':
323
                    $value = $location->pathString;
324 1
                    break;
325 1
                case 'priority':
326 1
                    $value = $location->priority;
327 1
                    break;
328 1
                case 'publication_date':
329 1
                    $value = $location->contentInfo->publishedDate->getTimestamp();
330 1
                    break;
331 1
                case 'section_id':
332 1
                    $value = $location->contentInfo->sectionId;
333 1
                    break;
334 View Code Duplication
                case 'section_identifier':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
335
                    $sectionService = $this->repository->getSectionService();
336 1
                    $value = $sectionService->loadSection($location->contentInfo->sectionId)->identifier;
337
                    break;
338 1
                case 'sort_field':
339 1
                    $value = $this->sortConverter->sortField2Hash($location->sortField);
340
                    break;
341 1
                case 'sort_order':
342
                    $value = $this->sortConverter->sortOrder2Hash($location->sortOrder);
343
                    break;
344
                default:
345
                    throw new \InvalidArgumentException('Location Manager does not support setting references for attribute ' . $reference['attribute']);
346
            }
347
348
            $overwrite = false;
349
            if (isset($reference['overwrite'])) {
350
                $overwrite = $reference['overwrite'];
351
            }
352
            $this->referenceResolver->addReference($reference['identifier'], $value, $overwrite);
353
        }
354
355
        return true;
356
    }
357
358
    /**
359
     * @param int|string|array $locationKey
360
     * @return Location
361
     */
362
    public function matchLocationByKey($locationKey)
363
    {
364
        return $this->locationMatcher->matchOneByKey($locationKey);
365
    }
366
367
    /**
368
     * NB: weirdly enough, it returns contents, not locations
369
     *
370
     * @param string $action
371
     * @return ContentCollection
372
     * @throws \Exception
373
     */
374
    protected function matchContents($action, $step)
375
    {
376
        if (!isset($step->dsl['object_id']) && !isset($step->dsl['remote_id']) && !isset($step->dsl['match'])) {
377
            throw new \Exception("The ID or remote ID of an object or a Match Condition is required to $action a new location.");
378
        }
379
380
        // Backwards compat
381
        if (!isset($step->dsl['match'])) {
382
            if (isset($step->dsl['object_id'])) {
383
                $step->dsl['match'] = array('content_id' => $step->dsl['object_id']);
384
            } elseif (isset($step->dsl['remote_id'])) {
385
                $step->dsl['match'] = array('content_remote_id' => $step->dsl['remote_id']);
386
            }
387
        }
388
389
        $match = $step->dsl['match'];
390
391
        // convert the references passed in the match
392
        foreach ($match as $condition => $values) {
393
            if (is_array($values)) {
394
                foreach ($values as $position => $value) {
395
                    $match[$condition][$position] = $this->referenceResolver->resolveReference($value);
396
                }
397
            } else {
398
                $match[$condition] = $this->referenceResolver->resolveReference($values);
399
            }
400
        }
401
402
        return $this->contentMatcher->matchContent($match);
403
    }
404
405
    protected function setMainLocation(Location $location)
406
    {
407
        $contentService = $this->repository->getContentService();
408
        $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
409
        $contentMetaDataUpdateStruct->mainLocationId = $location->id;
410
        $contentService->updateContentMetadata($location->contentInfo, $contentMetaDataUpdateStruct);
411
    }
412
413
    /**
414
     * @param $newValue
415
     * @param null $currentValue
416
     * @return int|null
417
     *
418
     * * @todo make protected
419
     */
420
    public function getSortField($newValue, $currentValue = null)
421
    {
422
        $sortField = $currentValue;
423
424
        if ($newValue !== null) {
425
            $sortField = $this->sortConverter->hash2SortField($newValue);
426
        }
427
428
        return $sortField;
429
    }
430
431
    /**
432
     * Get the sort order based on the current value and the value in the DSL definition.
433
     *
434
     * @see \eZ\Publish\API\Repository\Values\Content\Location::SORT_ORDER_*
435
     *
436
     * @param int $newValue
437
     * @param int $currentValue
438
     * @return int|null
439
     *
440
     * @todo make protected
441
     */
442
    public function getSortOrder($newValue, $currentValue = null)
443
    {
444
        $sortOrder = $currentValue;
445
446
        if ($newValue !== null) {
447
            $sortOrder = $this->sortConverter->hash2SortOrder($newValue);
448
        }
449
450
        return $sortOrder;
451
    }
452
453
}
454