Completed
Push — master ( d73db1...81955d )
by Gaetano
07:41
created

ContentManager   F

Complexity

Total Complexity 85

Size/Duplication

Total Lines 478
Duplicated Lines 7.95 %

Coupling/Cohesion

Components 1
Dependencies 22

Test Coverage

Coverage 84.18%

Importance

Changes 0
Metric Value
wmc 85
lcom 1
cbo 22
dl 38
loc 478
ccs 133
cts 158
cp 0.8418
rs 1.5789
c 0
b 0
f 0

How to fix   Duplicated Code    Complexity   

Duplicated Code

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

Common duplication problems, and corresponding solutions are:

Complex Class

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

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

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

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

1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 35 and the first side effect is on line 24.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
namespace Kaliop\eZMigrationBundle\Core\Executor;
4
5
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
6
use eZ\Publish\API\Repository\Values\ContentType\FieldDefinition;
7
use eZ\Publish\API\Repository\Values\Content\Content;
8
use eZ\Publish\API\Repository\Values\Content\ContentCreateStruct;
9
use eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct;
10
use eZ\Publish\Core\FieldType\Checkbox\Value as CheckboxValue;
11
use Kaliop\eZMigrationBundle\API\Collection\ContentCollection;
12
use Kaliop\eZMigrationBundle\Core\Matcher\ContentMatcher;
13
use Kaliop\eZMigrationBundle\Core\Matcher\SectionMatcher;
14
use Kaliop\eZMigrationBundle\Core\Matcher\UserMatcher;
15
use Kaliop\eZMigrationBundle\Core\Matcher\ObjectStateMatcher;
16
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
17
18
/**
19
 * Implements the actions for managing (create/update/delete) Content in the system through
20
 * migrations and abstracts away the eZ Publish Public API.
21
 *
22
 * @todo add support for updating of content metadata
23
 */
24
class ContentManager extends RepositoryExecutor
0 ignored issues
show
Bug introduced by
Possible parse error: class missing opening or closing brace
Loading history...
25
{
26
    protected $supportedStepTypes = array('content');
27 20
28
    protected $contentMatcher;
29 20
    protected $sectionMatcher;
30 20
    protected $userMatcher;
31 20
    protected $objectStateMatcher;
32
    protected $complexFieldManager;
33
    protected $locationManager;
34
35
    public function __construct(ContentMatcher $contentMatcher, SectionMatcher $sectionMatcher, UserMatcher $userMatcher,
36 4
        ObjectStateMatcher $objectStateMatcher, $complexFieldManager, $locationManager)
37
    {
38 4
        $this->contentMatcher = $contentMatcher;
39 4
        $this->sectionMatcher = $sectionMatcher;
40 4
        $this->userMatcher = $userMatcher;
41
        $this->objectStateMatcher = $objectStateMatcher;
42 4
        $this->complexFieldManager = $complexFieldManager;
43 4
        $this->locationManager = $locationManager;
44 3
    }
45 3
46 4
    /**
47
     * Handle the content create migration action type
48 4
     */
49 4
    protected function create()
50
    {
51 4
        $contentService = $this->repository->getContentService();
52 1
        $locationService = $this->repository->getLocationService();
53 1
        $contentTypeService = $this->repository->getContentTypeService();
54
55
        $contentTypeIdentifier = $this->dsl['content_type'];
56 4
        $contentTypeIdentifier = $this->referenceResolver->resolveReference($contentTypeIdentifier);
57 4
        /// @todo use a contenttypematcher
58 1
        $contentType = $contentTypeService->loadContentTypeByIdentifier($contentTypeIdentifier);
59 1
60 4
        $contentCreateStruct = $contentService->newContentCreateStruct($contentType, $this->getLanguageCode());
61 4
62 1
        $this->setFields($contentCreateStruct, $this->dsl['attributes'], $contentType);
63 1
64
        if (isset($this->dsl['always_available'])) {
65 4
            $contentCreateStruct->alwaysAvailable = $this->dsl['always_available'];
66 1
        } else {
67 1
            // Can be removed when https://github.com/kaliop-uk/ezmigrationbundle/pull/88 is merged
68
            $contentCreateStruct->alwaysAvailable = $contentType->defaultAlwaysAvailable;
69 4
        }
70
71 4
        if (isset($this->dsl['remote_id'])) {
72
            $contentCreateStruct->remoteId = $this->dsl['remote_id'];
73
        }
74
75
        if (isset($this->dsl['section'])) {
76
            $sectionId = $this->dsl['section'];
77
            $sectionId = $this->referenceResolver->resolveReference($sectionId);
78
            $contentCreateStruct->sectionId = $sectionId;
79
        }
80
81
        if (isset($this->dsl['owner'])) {
82
            $owner = $this->getUser($this->dsl['owner']);
83 4
            $contentCreateStruct->ownerId = $owner->id;
84 3
        }
85
86 3
        // This is a bit tricky, as the eZPublish API does not support having a different creator and owner with only 1 version.
87 3
        // We allow it, hoping that nothing gets broken because of it
88
        if (isset($this->dsl['version_creator'])) {
89
            $realContentOwnerId = $contentCreateStruct->ownerId;
90
            if ($realContentOwnerId == null) {
91
                $realContentOwnerId = $this->repository->getCurrentUser()->id;
92
            }
93
            $versionCreator = $this->getUser($this->dsl['version_creator']);
94 1
            $contentCreateStruct->ownerId = $versionCreator->id;
95
        }
96 1
97 1
        if (isset($this->dsl['modification_date'])) {
98
            $contentCreateStruct->modificationDate = $this->toDateTime($this->dsl['modification_date']);
99
        }
100
101
        // instantiate a location create struct from the parent location:
102
        // BC
103
        $locationId = isset($this->dsl['parent_location']) ? $this->dsl['parent_location'] : (
104
            isset($this->dsl['main_location']) ? $this->dsl['main_location'] : null
105
        );
106
        // 1st resolve references
107
        $locationId = $this->referenceResolver->resolveReference($locationId);
108
        // 2nd allow to specify the location via remote_id
109
        $locationId = $this->locationManager->matchLocationByKey($locationId)->id;
110
        $locationCreateStruct = $locationService->newLocationCreateStruct($locationId);
111
112
        if (isset($this->dsl['location_remote_id'])) {
113
            $locationCreateStruct->remoteId = $this->dsl['location_remote_id'];
114
        }
115
116
        if (isset($this->dsl['priority'])) {
117
            $locationCreateStruct->priority = $this->dsl['priority'];
118
        }
119
120
        if (isset($this->dsl['is_hidden'])) {
121 1
            $locationCreateStruct->hidden = $this->dsl['is_hidden'];
122
        }
123 1
124
        if (isset($this->dsl['sort_field'])) {
125
            $locationCreateStruct->sortField = $this->locationManager->getSortField($this->dsl['sort_field']);
126
        } else {
127 1
            $locationCreateStruct->sortField = $contentType->defaultSortField;
128
        }
129 1
130 1
        if (isset($this->dsl['sort_order'])) {
131
            $locationCreateStruct->sortOrder = $this->locationManager->getSortOrder($this->dsl['sort_order']);
132 1
        } else {
133 1
            $locationCreateStruct->sortOrder = $contentType->defaultSortOrder;
134 1
        }
135
136 1
        $locations = array($locationCreateStruct);
137
138 1
        // BC
139 1
        $other_locations = isset($this->dsl['other_parent_locations']) ? $this->dsl['other_parent_locations'] : (
140 1
            isset($this->dsl['other_locations']) ? $this->dsl['other_locations'] : null
141
        );
142 1
        if (isset($other_locations)) {
143 1
            foreach ($other_locations as $locationId) {
144 1
                $locationId = $this->referenceResolver->resolveReference($locationId);
145
                $locationId = $this->locationManager->matchLocationByKey($locationId)->id;
146 1
                $secondaryLocationCreateStruct = $locationService->newLocationCreateStruct($locationId);
147
                array_push($locations, $secondaryLocationCreateStruct);
148
            }
149
        }
150
151
        // create a draft using the content and location create struct and publish it
152
        $draft = $contentService->createContent($contentCreateStruct, $locations);
153
        $content = $contentService->publishVersion($draft->versionInfo);
154
155
        if (isset($this->dsl['object_states'])) {
156
            $this->setObjectStates($content, $this->dsl['object_states']);
157
        }
158
159
        // 2nd part of the hack: re-set the content owner to its intended value
160
        if (isset($this->dsl['version_creator']) || isset($this->dsl['publication_date'])) {
161 1
            $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
162 1
163 1
            if (isset($this->dsl['version_creator'])) {
164
                $contentMetaDataUpdateStruct->ownerId = $realContentOwnerId;
165
            }
166
            if (isset($this->dsl['publication_date'])) {
167
                $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($this->dsl['publication_date']);
168 3
            }
169
170 3
            $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct);
171
        }
172 3
173
        $this->setReferences($content);
174 3
175
        return $content;
176 3
    }
177 3
178
    /**
179
     * Handle the content update migration action type
180
     *
181 3
     * @todo handle updating of more metadata fields
182 3
     */
183
    protected function update()
184
    {
185
        $contentService = $this->repository->getContentService();
186
        $contentTypeService = $this->repository->getContentTypeService();
187
188
        $contentCollection = $this->matchContents('update');
189 3
190
        if (count($contentCollection) > 1 && isset($this->dsl['references'])) {
191 3
            throw new \Exception("Can not execute Content update because multiple contents match, and a references section is specified in the dsl. References can be set when only 1 content matches");
192
        }
193
194
        $contentType = null;
195
196 3
        foreach ($contentCollection as $key => $content) {
197 1
            $contentInfo = $content->contentInfo;
198 1
199 1
            if ($contentType == null) {
200
                $contentType = $contentTypeService->loadContentType($contentInfo->contentTypeId);
201
            }
202 1
203
            $contentUpdateStruct = $contentService->newContentUpdateStruct();
204 3
205
            if (isset($this->dsl['attributes'])) {
206
                $this->setFields($contentUpdateStruct, $this->dsl['attributes'], $contentType);
207 3
            }
208 3
209 1
            $versionCreator = null;
210 1
            if (isset($this->dsl['version_creator'])) {
211 1
                $versionCreator = $this->getUser($this->dsl['version_creator']);
212 1
            }
213 1
214 1
            $draft = $contentService->createContentDraft($contentInfo, null, $versionCreator);
215 3
            $contentService->updateContent($draft->versionInfo, $contentUpdateStruct);
216 3
            $content = $contentService->publishVersion($draft->versionInfo);
217 3
218
            if (isset($this->dsl['new_remote_id']) || isset($this->dsl['new_remote_id']) ||
219 3
                isset($this->dsl['modification_date']) || isset($this->dsl['publication_date'])) {
220
221 3
                $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
222
223
                if (isset($this->dsl['new_remote_id'])) {
224
                    $contentMetaDataUpdateStruct->remoteId = $this->dsl['new_remote_id'];
225
                }
226
227
                if (isset($this->dsl['owner'])) {
228
                    $owner = $this->getUser($this->dsl['owner']);
229
                    $contentMetaDataUpdateStruct->ownerId = $owner->id;
230
                }
231 4
232
                if (isset($this->dsl['modification_date'])) {
233 4
                    $contentMetaDataUpdateStruct->modificationDate = $this->toDateTime($this->dsl['modification_date']);
234
                }
235
236 4
                if (isset($this->dsl['publication_date'])) {
237
                    $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($this->dsl['publication_date']);
238 4
                }
239
240 4
                $content = $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct);
241
            }
242 1
243 1
            if (isset($this->dsl['section'])) {
244
                $this->setSection($content, $this->dsl['section']);
245 4
            }
246 3
247
            if (isset($this->dsl['object_states'])) {
248 4
                $this->setObjectStates($content, $this->dsl['object_states']);
249 4
            }
250 4
251
            $contentCollection[$key] = $content;
252
        }
253
254
        $this->setReferences($contentCollection);
255
256
        return $contentCollection;
257
    }
258
259
    /**
260
     * Handle the content delete migration action type
261
     */
262 4
    protected function delete()
263
    {
264 4
        $contentService = $this->repository->getContentService();
265 4
266
        $contentCollection = $this->matchContents('delete');
267
268 4
        foreach ($contentCollection as $content) {
269 4
            try {
270 4
                $contentService->deleteContent($content->contentInfo);
271
            } catch (NotFoundException $e) {
272 4
                // Someone else (or even us, by virtue of location tree?) removed the content which we found just a
273 1
                // second ago. We can safely ignore this
274 1
            }
275
        }
276 4
277
        return $contentCollection;
278
    }
279
280
    /**
281
     * @param string $action
282
     * @return ContentCollection
283
     * @throws \Exception
284
     */
285
    protected function matchContents($action)
286
    {
287 1
        if (!isset($this->dsl['object_id']) && !isset($this->dsl['remote_id']) && !isset($this->dsl['match'])) {
288
            throw new \Exception("The ID or remote ID of an object or a Match Condition is required to $action a new location.");
289 1
        }
290 1
291
        // Backwards compat
292
        if (!isset($this->dsl['match'])) {
293
            if (isset($this->dsl['object_id'])) {
294
                $this->dsl['match'] = array('content_id' => $this->dsl['object_id']);
295
            } elseif (isset($this->dsl['remote_id'])) {
296
                $this->dsl['match'] = array('content_remote_id' => $this->dsl['remote_id']);
297
            }
298
        }
299
300
        $match = $this->dsl['match'];
301
302 3
        // convert the references passed in the match
303
        foreach ($match as $condition => $values) {
304 3
            if (is_array($values)) {
305 2
                foreach ($values as $position => $value) {
306
                    $match[$condition][$position] = $this->referenceResolver->resolveReference($value);
307
                }
308 3
            } else {
309
                $match[$condition] = $this->referenceResolver->resolveReference($values);
310
            }
311
        }
312
313
        return $this->contentMatcher->match($match);
314
    }
315 3
316
    /**
317 3
     * Helper function to set the fields of a ContentCreateStruct based on the DSL attribute settings.
318 3
     *
319 3
     * @param ContentCreateStruct|ContentUpdateStruct $createOrUpdateStruct
320 3
     * @param ContentType $contentType
321 3
     * @param array $fields see description of expected format in code below
322 3
     * @throws \Exception
323 2
     */
324 2
    protected function setFields($createOrUpdateStruct, array $fields, ContentType $contentType)
325 2
    {
326 1
        $i = 0;
327 1
        // the 'easy' yml: key = field name, value = value
328 1
        // deprecated: the 'legacy' yml: key = numerical index, value = array ( field name => value )
329 1
        foreach ($fields as $key => $field) {
330
331
            if ($key === $i && is_array($field) && count($field) == 1) {
332
                // each $field is one key value pair
333 3
                // 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...
334
                reset($field);
335 3
                $fieldIdentifier = key($field);
336 3
                $fieldValue = $field[$fieldIdentifier];
337
            } else {
338 3
                $fieldIdentifier = $key;
339
                $fieldValue = $field;
340
            }
341
342
            if (!isset($contentType->fieldDefinitionsByIdentifier[$fieldIdentifier])) {
343
                throw new \Exception("Field '$fieldIdentifier' is not present in field type '{$contentType->identifier}'")
344
345
            $fieldDefinition = $contentType->fieldDefinitionsByIdentifier[$fieldIdentifier];
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_VARIABLE
Loading history...
346
            $fieldValue = $this->getFieldValue($fieldValue, $fieldDefinition, $contentType->identifier, $this->context);
347
348
            $createOrUpdateStruct->setField($fieldIdentifier, $fieldValue, $this->getLanguageCode());
349
350
            $i++;
351
        }
352
    }
353
354
    protected function setSection(Content $content, $sectionKey)
355
    {
356
        $sectionKey = $this->referenceResolver->resolveReference($sectionKey);
357
        $section = $this->sectionMatcher->matchOneByKey($sectionKey);
358
359
        $sectionService = $this->repository->getSectionService();
360
        $sectionService->assignSection($content->contentInfo, $section);
361
    }
362
363
    protected function setObjectStates(Content $content, array $stateKeys)
364
    {
365
        foreach ($stateKeys as $stateKey) {
366
            $stateKey = $this->referenceResolver->resolveReference($stateKey);
367
            /** @var \eZ\Publish\API\Repository\Values\ObjectState\ObjectState $state */
368
            $state = $this->objectStateMatcher->matchOneByKey($stateKey);
369
370
            $stateService = $this->repository->getObjectStateService();
371
            $stateService->setContentState($content->contentInfo, $state->getObjectStateGroup(), $state);
372
        }
373
    }
374
375
    /**
376
     * Create the field value for either a primitive (ie. scalar) or complex field
377
     *
378
     * @param mixed $value
379
     * @param FieldDefinition $fieldDefinition
380
     * @param string $contentTypeIdentifier
381
     * @param array $context
382
     * @throws \InvalidArgumentException
383
     * @return mixed
384
     */
385
    protected function getFieldValue($value, FieldDefinition $fieldDefinition, $contentTypeIdentifier, array $context = array())
386
    {
387
        $fieldTypeIdentifier = $fieldDefinition->fieldTypeIdentifier;
388
        if (is_array($value) || $this->complexFieldManager->managesField($fieldTypeIdentifier, $contentTypeIdentifier)) {
389
            return $this->complexFieldManager->getComplexFieldValue($fieldTypeIdentifier, $contentTypeIdentifier, $value, $context);
390
        }
391
392
        return $this->getSingleFieldValue($value, $fieldDefinition, $contentTypeIdentifier, $context);
393
    }
394
395
    /**
396
     * Create the field value for a primitive field
397
     * This function is needed to get past validation on Checkbox fieldtype (eZP bug)
398
     *
399
     * @param mixed $value
400
     * @param FieldDefinition $fieldDefinition
401
     * @param string $contentTypeIdentifier
402
     * @param array $context
403
     * @throws \InvalidArgumentException
404
     * @return mixed
405
     */
406
    protected function getSingleFieldValue($value, FieldDefinition $fieldDefinition, $contentTypeIdentifier, array $context = array())
407
    {
408
        $fieldTypeIdentifier = $fieldDefinition->fieldTypeIdentifier;
409
        switch ($fieldTypeIdentifier) {
410
            case 'ezboolean':
411
                $value = new CheckboxValue(($value == 1) ? true : false);
412
                break;
413
            default:
414
                // do nothing
415
        }
416
417
        // q: do we really want this to happen by default on all scalar field values?
418
        // Note: if you want this *not* to happen, register a complex field for your scalar field...
419
        $value = $this->referenceResolver->resolveReference($value);
420
421
        return $value;
422
    }
423
424
    /**
425
     * Load user using either login, email, id - resolving eventual references
426
     * @param int|string $userKey
427
     * @return \eZ\Publish\API\Repository\Values\User\User
428
     */
429
    protected function getUser($userKey)
430
    {
431
        $userKey = $this->referenceResolver->resolveReference($userKey);
432
        return $this->userMatcher->matchOneByKey($userKey);
433
    }
434
435
    /**
436
     * Sets references to certain content attributes.
437
     * The Content Manager currently supports setting references to object_id and location_id
438
     *
439
     * @param \eZ\Publish\API\Repository\Values\Content\Content|ContentCollection $content
440
     * @throws \InvalidArgumentException When trying to set a reference to an unsupported attribute
441
     * @return boolean
442
     *
443
     * @todo add support for other attributes: remote ids, contentTypeId, contentTypeIdentifier, section, etc...
444
     */
445
    protected function setReferences($content)
446
    {
447
        if (!array_key_exists('references', $this->dsl)) {
448
            return false;
449
        }
450
451
        if ($content instanceof ContentCollection) {
452
            if (count($content) > 1) {
453
                throw new \InvalidArgumentException('Content Manager does not support setting references for creating/updating of multiple contents');
454
            }
455
            $content = reset($content);
456
        }
457
458
        foreach ($this->dsl['references'] as $reference) {
459
460
            switch ($reference['attribute']) {
461
                case 'object_id':
462
                case 'content_id':
463
                case 'id':
464
                    $value = $content->id;
465
                    break;
466
                case 'remote_id':
467
                case 'content_remote_id':
468
                    $value = $content->contentInfo->remoteId;
469
                    break;
470
                case 'location_id':
471
                    $value = $content->contentInfo->mainLocationId;
472
                    break;
473
                case 'path':
474
                    $locationService = $this->repository->getLocationService();
475
                    $value = $locationService->loadLocation($content->contentInfo->mainLocationId)->pathString;
476
                    break;
477
                default:
478
                    throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute']);
479
            }
480
481
            $this->referenceResolver->addReference($reference['identifier'], $value);
482
        }
483
484
        return true;
485
    }
486
487
    /**
488
     * @param int|string $date if integer, we assume a timestamp
489
     * @return \DateTime
490
     */
491
    protected function toDateTime($date)
492
    {
493
        if (is_int($date)) {
494
            return new \DateTime("@" . $date);
495
        } else {
496
            return new \DateTime($date);
497
        }
498
    }
499
}
500