Completed
Push — master ( 1ee05e...99144a )
by Gaetano
05:13
created

ContentManager::getObjectStates()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 16
c 0
b 0
f 0
ccs 0
cts 10
cp 0
rs 9.7333
cc 3
nc 3
nop 1
crap 12
1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Core\Executor;
4
5
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
6
use eZ\Publish\API\Repository\Values\ContentType\FieldDefinition;
7
use eZ\Publish\API\Repository\Values\Content\Location;
8
use eZ\Publish\API\Repository\Values\Content\Content;
9
use eZ\Publish\API\Repository\Values\Content\ContentCreateStruct;
10
use eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct;
11
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
12
use Kaliop\eZMigrationBundle\API\Collection\ContentCollection;
13
use Kaliop\eZMigrationBundle\API\MigrationGeneratorInterface;
14
use Kaliop\eZMigrationBundle\Core\FieldHandlerManager;
15
use Kaliop\eZMigrationBundle\Core\Matcher\ContentMatcher;
16
use Kaliop\eZMigrationBundle\Core\Matcher\SectionMatcher;
17
use Kaliop\eZMigrationBundle\Core\Matcher\UserMatcher;
18
use Kaliop\eZMigrationBundle\Core\Matcher\ObjectStateMatcher;
19
use Kaliop\eZMigrationBundle\Core\Matcher\ObjectStateGroupMatcher;
20
use Kaliop\eZMigrationBundle\Core\Helper\SortConverter;
21
use JmesPath\Env as JmesPath;
22
23
/**
24
 * Handles content migrations.
25
 *
26
 * @todo add support for updating of content metadata
27
 */
28
class ContentManager extends RepositoryExecutor implements MigrationGeneratorInterface
29
{
30
    protected $supportedStepTypes = array('content');
31
    protected $supportedActions = array('create', 'load', 'update', 'delete');
32
33
    protected $contentMatcher;
34
    protected $sectionMatcher;
35
    protected $userMatcher;
36
    protected $objectStateMatcher;
37
    protected $objectStateGroupMatcher;
38
    protected $fieldHandlerManager;
39
    protected $locationManager;
40
    protected $sortConverter;
41
42
    // these are not exported when generating a migration
43
    protected $ignoredStateGroupIdentifiers = array('ez_lock');
44
45 80 View Code Duplication
    public function __construct(
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...
46
        ContentMatcher $contentMatcher,
47
        SectionMatcher $sectionMatcher,
48
        UserMatcher $userMatcher,
49
        ObjectStateMatcher $objectStateMatcher,
50
        ObjectStateGroupMatcher $objectStateGroupMatcher,
51
        FieldHandlerManager $fieldHandlerManager,
52
        LocationManager $locationManager,
53
        SortConverter $sortConverter
54
    ) {
55 80
        $this->contentMatcher = $contentMatcher;
56 80
        $this->sectionMatcher = $sectionMatcher;
57 80
        $this->userMatcher = $userMatcher;
58 80
        $this->objectStateMatcher = $objectStateMatcher;
59 80
        $this->objectStateGroupMatcher = $objectStateGroupMatcher;
60 80
        $this->fieldHandlerManager = $fieldHandlerManager;
61 80
        $this->locationManager = $locationManager;
62 80
        $this->sortConverter = $sortConverter;
63 80
    }
64
65
    /**
66
     * Handles the content create migration action type
67
     */
68
    protected function create($step)
69
    {
70
        $contentService = $this->repository->getContentService();
71
        $locationService = $this->repository->getLocationService();
72
        $contentTypeService = $this->repository->getContentTypeService();
73
74
        $contentTypeIdentifier = $step->dsl['content_type'];
75
        $contentTypeIdentifier = $this->referenceResolver->resolveReference($contentTypeIdentifier);
76
        /// @todo use a contenttypematcher
77
        $contentType = $contentTypeService->loadContentTypeByIdentifier($contentTypeIdentifier);
78
79
        $contentCreateStruct = $contentService->newContentCreateStruct($contentType, $this->getLanguageCode($step));
80
81
        $this->setFields($contentCreateStruct, $step->dsl['attributes'], $contentType, $step);
82
83
        if (isset($step->dsl['always_available'])) {
84
            $contentCreateStruct->alwaysAvailable = $step->dsl['always_available'];
85
        } else {
86
            // Could be removed when https://github.com/ezsystems/ezpublish-kernel/pull/1874 is merged,
87
            // but we strive to support old eZ kernel versions as well...
88
            $contentCreateStruct->alwaysAvailable = $contentType->defaultAlwaysAvailable;
89
        }
90
91
        if (isset($step->dsl['remote_id'])) {
92
            $contentCreateStruct->remoteId = $step->dsl['remote_id'];
93
        }
94
95 View Code Duplication
        if (isset($step->dsl['section'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
96
            $sectionKey = $this->referenceResolver->resolveReference($step->dsl['section']);
97
            $section = $this->sectionMatcher->matchOneByKey($sectionKey);
98
            $contentCreateStruct->sectionId = $section->id;
99
        }
100
101 View Code Duplication
        if (isset($step->dsl['owner'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
102
            $owner = $this->getUser($step->dsl['owner']);
103
            $contentCreateStruct->ownerId = $owner->id;
104
        }
105
106
        // This is a bit tricky, as the eZPublish API does not support having a different creator and owner with only 1 version.
107
        // We allow it, hoping that nothing gets broken because of it
108
        if (isset($step->dsl['version_creator'])) {
109
            $realContentOwnerId = $contentCreateStruct->ownerId;
110
            if ($realContentOwnerId == null) {
111
                $realContentOwnerId = $this->repository->getCurrentUser()->id;
112
            }
113
            $versionCreator = $this->getUser($step->dsl['version_creator']);
114
            $contentCreateStruct->ownerId = $versionCreator->id;
115
        }
116
117
        if (isset($step->dsl['modification_date'])) {
118
            $contentCreateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']);
119
        }
120
121
        // instantiate a location create struct from the parent location:
122
        // BC
123
        $locationId = isset($step->dsl['parent_location']) ? $step->dsl['parent_location'] : (
124
            isset($step->dsl['main_location']) ? $step->dsl['main_location'] : null
125
        );
126
        // 1st resolve references
127
        $locationId = $this->referenceResolver->resolveReference($locationId);
128
        // 2nd allow to specify the location via remote_id
129
        $locationId = $this->locationManager->matchLocationByKey($locationId)->id;
130
        $locationCreateStruct = $locationService->newLocationCreateStruct($locationId);
131
132
        if (isset($step->dsl['location_remote_id'])) {
133
            $locationCreateStruct->remoteId = $step->dsl['location_remote_id'];
134
        }
135
136
        if (isset($step->dsl['priority'])) {
137
            $locationCreateStruct->priority = $step->dsl['priority'];
138
        }
139
140
        if (isset($step->dsl['is_hidden'])) {
141
            $locationCreateStruct->hidden = $step->dsl['is_hidden'];
142
        }
143
144
        if (isset($step->dsl['sort_field'])) {
145
            $locationCreateStruct->sortField = $this->sortConverter->hash2SortField($step->dsl['sort_field']);
146
        } else {
147
            $locationCreateStruct->sortField = $contentType->defaultSortField;
148
        }
149
150
        if (isset($step->dsl['sort_order'])) {
151
            $locationCreateStruct->sortOrder = $this->sortConverter->hash2SortOrder($step->dsl['sort_order']);
152
        } else {
153
            $locationCreateStruct->sortOrder = $contentType->defaultSortOrder;
154
        }
155
156
        $locations = array($locationCreateStruct);
157
158
        // BC
159
        $other_locations = isset($step->dsl['other_parent_locations']) ? $step->dsl['other_parent_locations'] : (
160
            isset($step->dsl['other_locations']) ? $step->dsl['other_locations'] : null
161
        );
162
        if (isset($other_locations)) {
163
            foreach ($other_locations as $locationId) {
164
                $locationId = $this->referenceResolver->resolveReference($locationId);
165
                $locationId = $this->locationManager->matchLocationByKey($locationId)->id;
166
                $secondaryLocationCreateStruct = $locationService->newLocationCreateStruct($locationId);
167
                array_push($locations, $secondaryLocationCreateStruct);
168
            }
169
        }
170
171
        // create a draft using the content and location create struct and publish it
172
        $draft = $contentService->createContent($contentCreateStruct, $locations);
173
        $content = $contentService->publishVersion($draft->versionInfo);
174
175
        if (isset($step->dsl['object_states'])) {
176
            $this->setObjectStates($content, $step->dsl['object_states']);
177
        }
178
179
        // 2nd part of the hack: re-set the content owner to its intended value
180
        if (isset($step->dsl['version_creator']) || isset($step->dsl['publication_date'])) {
181
            $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
182
183
            if (isset($step->dsl['version_creator'])) {
184
                $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...
185
            }
186
            if (isset($step->dsl['publication_date'])) {
187
                $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($step->dsl['publication_date']);
188
            }
189
            // we have to do this to make sure we preserve the custom modification date
190
            if (isset($step->dsl['modification_date'])) {
191
                $contentMetaDataUpdateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']);
192
            }
193
194
            $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct);
195
        }
196
197
        $this->setReferences($content, $step);
0 ignored issues
show
Documentation introduced by
$content is of type object<eZ\Publish\API\Re...Values\Content\Content>, but the function expects a object<Object>|object<Ka...ion\AbstractCollection>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
198
199
        return $content;
200
    }
201
202
    protected function load($step)
203
    {
204
        $contentCollection = $this->matchContents('load', $step);
205
206
        $this->setReferences($contentCollection, $step);
207
208
        return $contentCollection;
209
    }
210
211
    /**
212
     * Handles the content update migration action type
213
     *
214
     * @todo handle updating of more metadata fields
215
     */
216
    protected function update($step)
217
    {
218
        $contentService = $this->repository->getContentService();
219
        $contentTypeService = $this->repository->getContentTypeService();
220
221
        $contentCollection = $this->matchContents('update', $step);
222
223
        if (count($contentCollection) > 1 && isset($step->dsl['references'])) {
224
            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");
225
        }
226
227
        if (count($contentCollection) > 1 && isset($step->dsl['main_location'])) {
228
            throw new \Exception("Can not execute Content update because multiple contents match, and a main_location section is specified in the dsl. References can be set when only 1 content matches");
229
        }
230
231
        $contentType = array();
232
233
        foreach ($contentCollection as $key => $content) {
234
            $contentInfo = $content->contentInfo;
235
236
            if (!isset($contentType[$contentInfo->contentTypeId])) {
237
                $contentType[$contentInfo->contentTypeId] = $contentTypeService->loadContentType($contentInfo->contentTypeId);
238
            }
239
240
            if (isset($step->dsl['attributes']) || isset($step->dsl['version_creator'])) {
241
                $contentUpdateStruct = $contentService->newContentUpdateStruct();
242
243
                if (isset($step->dsl['attributes'])) {
244
                    $this->setFields($contentUpdateStruct, $step->dsl['attributes'], $contentType[$contentInfo->contentTypeId], $step);
245
                }
246
247
                $versionCreator = null;
248
                if (isset($step->dsl['version_creator'])) {
249
                    $versionCreator = $this->getUser($step->dsl['version_creator']);
250
                }
251
252
                $draft = $contentService->createContentDraft($contentInfo, null, $versionCreator);
253
                $contentService->updateContent($draft->versionInfo, $contentUpdateStruct);
254
                $content = $contentService->publishVersion($draft->versionInfo);
255
            }
256
257
            if (isset($step->dsl['always_available']) ||
258
                isset($step->dsl['new_remote_id']) ||
259
                isset($step->dsl['owner']) ||
260
                isset($step->dsl['modification_date']) ||
261
                isset($step->dsl['publication_date'])) {
262
263
                $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
264
265
                if (isset($step->dsl['always_available'])) {
266
                    $contentMetaDataUpdateStruct->alwaysAvailable = $step->dsl['always_available'];
267
                }
268
269
                if (isset($step->dsl['new_remote_id'])) {
270
                    $contentMetaDataUpdateStruct->remoteId = $step->dsl['new_remote_id'];
271
                }
272
273 View Code Duplication
                if (isset($step->dsl['owner'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
274
                    $owner = $this->getUser($step->dsl['owner']);
275
                    $contentMetaDataUpdateStruct->ownerId = $owner->id;
276
                }
277
278
                if (isset($step->dsl['modification_date'])) {
279
                    $contentMetaDataUpdateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']);
280
                }
281
282
                if (isset($step->dsl['publication_date'])) {
283
                    $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($step->dsl['publication_date']);
284
                }
285
286
                $content = $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct);
287
            }
288
289
            if (isset($step->dsl['section'])) {
290
                $this->setSection($content, $step->dsl['section']);
291
            }
292
293
            if (isset($step->dsl['object_states'])) {
294
                $this->setObjectStates($content, $step->dsl['object_states']);
295
            }
296
297
            if (isset($step->dsl['main_location'])) {
298
                $this->setMainLocation($content, $step->dsl['main_location']);
299
300
            }
301
            $contentCollection[$key] = $content;
302
        }
303
304
        $this->setReferences($contentCollection, $step);
305
306
        return $contentCollection;
307
    }
308
309
    /**
310
     * Handles the content delete migration action type
311
     */
312 View Code Duplication
    protected function delete($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...
313
    {
314
        $contentCollection = $this->matchContents('delete', $step);
315
316
        $this->setReferences($contentCollection, $step);
317
318
        $contentService = $this->repository->getContentService();
319
320
        foreach ($contentCollection as $content) {
321
            try {
322
                $contentService->deleteContent($content->contentInfo);
323
            } catch (NotFoundException $e) {
324
                // Someone else (or even us, by virtue of location tree?) removed the content which we found just a
325
                // second ago. We can safely ignore this
326
            }
327
        }
328
329
        return $contentCollection;
330
    }
331
332
    /**
333
     * @param string $action
334
     * @return ContentCollection
335
     * @throws \Exception
336
     */
337
    protected function matchContents($action, $step)
338
    {
339 View Code Duplication
        if (!isset($step->dsl['object_id']) && !isset($step->dsl['remote_id']) && !isset($step->dsl['match'])) {
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...
340
            throw new \Exception("The id or remote id of an object or a match condition is required to $action a content");
341
        }
342
343
        // Backwards compat
344
345 View Code Duplication
        if (isset($step->dsl['match'])) {
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...
346
            $match = $step->dsl['match'];
347
        } else {
348
            if (isset($step->dsl['object_id'])) {
349
                $match = array('content_id' => $step->dsl['object_id']);
350
            } elseif (isset($step->dsl['remote_id'])) {
351
                $match = array('content_remote_id' => $step->dsl['remote_id']);
352
            }
353
        }
354
355
        // convert the references passed in the match
356
        $match = $this->resolveReferencesRecursively($match);
0 ignored issues
show
Bug introduced by
The variable $match does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
357
358
        $offset = isset($step->dsl['match_offset']) ? $this->referenceResolver->resolveReference($step->dsl['match_offset']) : 0;
359
        $limit = isset($step->dsl['match_limit']) ? $this->referenceResolver->resolveReference($step->dsl['match_limit']) : 0;
360
        $sort = isset($step->dsl['match_sort']) ? $this->referenceResolver->resolveReference($step->dsl['match_sort']) : array();
361
362
        return $this->contentMatcher->match($match, $sort, $offset, $limit);
363
    }
364
365
    /**
366
     * @param Content $content
367
     * @param array $references the definitions of the references to set
368
     * @throws \InvalidArgumentException When trying to assign a reference to an unsupported attribute
369
     * @return array key: the reference names, values: the reference values
370
     */
371
    protected function getReferencesValues($content, array $references, $step)
372
    {
373
        $refs = array();
374
375
        foreach ($references as $reference) {
376
377
            switch ($reference['attribute']) {
378
                case 'object_id':
379
                case 'content_id':
380
                case 'id':
381
                    $value = $content->id;
382
                    break;
383
                case 'remote_id':
384
                case 'content_remote_id':
385
                    $value = $content->contentInfo->remoteId;
386
                    break;
387
                case 'always_available':
388
                    $value = $content->contentInfo->alwaysAvailable;
389
                    break;
390
                case 'content_type_id':
391
                    $value = $content->contentInfo->contentTypeId;
392
                    break;
393
                case 'content_type_identifier':
394
                    $contentTypeService = $this->repository->getContentTypeService();
395
                    $value = $contentTypeService->loadContentType($content->contentInfo->contentTypeId)->identifier;
396
                    break;
397
                case 'current_version':
398
                case 'current_version_no':
399
                    $value = $content->contentInfo->currentVersionNo;
400
                    break;
401
                case 'location_id':
402
                case 'main_location_id':
403
                    $value = $content->contentInfo->mainLocationId;
404
                    break;
405
                case 'main_language_code':
406
                    $value = $content->contentInfo->mainLanguageCode;
407
                    break;
408
                case 'modification_date':
409
                    $value = $content->contentInfo->modificationDate->getTimestamp();
410
                    break;
411
                case 'name':
412
                    $value = $content->contentInfo->name;
413
                    break;
414
                case 'owner_id':
415
                    $value = $content->contentInfo->ownerId;
416
                    break;
417
                case 'path':
418
                    $locationService = $this->repository->getLocationService();
419
                    $value = $locationService->loadLocation($content->contentInfo->mainLocationId)->pathString;
420
                    break;
421
                case 'publication_date':
422
                    $value = $content->contentInfo->publishedDate->getTimestamp();
423
                    break;
424
                case 'section_id':
425
                    $value = $content->contentInfo->sectionId;
426
                    break;
427
                case 'section_identifier':
428
                    $sectionService = $this->repository->getSectionService();
429
                    $value = $sectionService->loadSection($content->contentInfo->sectionId)->identifier;
430
                    break;
431
                case 'version_count':
432
                    $contentService = $this->repository->getContentService();
433
                    $value = count($contentService->loadVersions($content->contentInfo));
434
                    break;
435
                default:
436
                    if (strpos($reference['attribute'], 'object_state.') === 0) {
437
                        $stateGroupKey = substr($reference['attribute'], 13);
438
                        $stateGroup = $this->objectStateGroupMatcher->matchOneByKey($stateGroupKey);
439
                        $value = $stateGroupKey . '/' . $this->repository->getObjectStateService()->
440
                            getContentState($content->contentInfo, $stateGroup)->identifier;
441
                        break;
442
                    }
443
444
                    // allow to get the value of fields as well as their sub-parts
445
                    if (strpos($reference['attribute'], 'attributes.') === 0) {
446
                        $contentType = $this->repository->getContentTypeService()->loadContentType(
447
                            $content->contentInfo->contentTypeId
448
                        );
449
                        $parts = explode('.', $reference['attribute']);
450
                        // totally not sure if this list of special chars is correct for what could follow a jmespath identifier...
451
                        // also what about quoted strings?
452
                        $fieldIdentifier = preg_replace('/[[(|&!{].*$/', '', $parts[1]);
453
                        $field = $content->getField($fieldIdentifier);
0 ignored issues
show
Bug introduced by
The method getField() does not exist on eZ\Publish\API\Repository\Values\Content\Content. Did you maybe mean getFields()?

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

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

Loading history...
454
                        $fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier);
455
                        $hashValue = $this->fieldHandlerManager->fieldValueToHash(
456
                            $fieldDefinition->fieldTypeIdentifier, $contentType->identifier, $field->value
457
                        );
458
                        if (is_array($hashValue)) {
459 View Code Duplication
                            if (count($parts) == 2 && $fieldIdentifier === $parts[1]) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
460
                                throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute'] . ': the given attribute has an array value');
461
                            }
462
                            $value = JmesPath::search(implode('.', array_slice($parts, 1)), array($fieldIdentifier => $hashValue));
463
                        } else {
464
                            if (count($parts) > 2) {
465
                                throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute'] . ': the given attribute has a scalar value');
466
                            }
467
                            $value = $hashValue;
468
                        }
469
                        break;
470
                    }
471
472
                    throw new \InvalidArgumentException('Content Manager does not support setting references for attribute ' . $reference['attribute']);
473
            }
474
475
            $refs[$reference['identifier']] = $value;
476
        }
477
478
        return $refs;
479
    }
480
481
    /**
482
     * @param array $matchCondition
483
     * @param string $mode
484
     * @param array $context
485
     * @throws \Exception
486
     * @return array
487
     *
488
     * @todo add support for dumping all object languages
489
     * @todo add 2ndary locations when in 'update' mode
490
     * @todo add dumping of sort_field and sort_order for 2ndary locations
491
     */
492
    public function generateMigration(array $matchCondition, $mode, array $context = array())
493
    {
494
        $previousUserId = $this->loginUser($this->getAdminUserIdentifierFromContext($context));
495
        $contentCollection = $this->contentMatcher->match($matchCondition);
496
        $data = array();
497
498
        /** @var \eZ\Publish\API\Repository\Values\Content\Content $content */
499
        foreach ($contentCollection as $content) {
500
501
            $location = $this->repository->getLocationService()->loadLocation($content->contentInfo->mainLocationId);
502
            $contentType = $this->repository->getContentTypeService()->loadContentType(
503
                $content->contentInfo->contentTypeId
504
            );
505
506
            $contentData = array(
507
                'type' => reset($this->supportedStepTypes),
508
                'mode' => $mode
509
            );
510
511
            switch ($mode) {
512
                case 'create':
513
                    $contentData = array_merge(
514
                        $contentData,
515
                        array(
516
                            'content_type' => $contentType->identifier,
517
                            'parent_location' => $location->parentLocationId,
518
                            'priority' => $location->priority,
519
                            'is_hidden' => $location->invisible,
520
                            'sort_field' => $this->sortConverter->sortField2Hash($location->sortField),
521
                            'sort_order' => $this->sortConverter->sortOrder2Hash($location->sortOrder),
522
                            'remote_id' => $content->contentInfo->remoteId,
523
                            'location_remote_id' => $location->remoteId,
524
                            'section' => $content->contentInfo->sectionId,
525
                            'object_states' => $this->getObjectStates($content),
526
                        )
527
                    );
528
                    $locationService = $this->repository->getLocationService();
529
                    /// @todo for accurate replication, we should express the addinfg of 2ndary locatins as separate steps, and copy over visibility, priority etc
530
                    $locations = $locationService->loadLocations($content->contentInfo);
531
                    if (count($locations) > 1) {
532
                        $otherParentLocations = array();
533
                        foreach($locations as $otherLocation) {
534
                            if ($otherLocation->id != $location->id) {
535
                                $otherParentLocations[] = $otherLocation->parentLocationId;
536
                            }
537
                        }
538
                        $contentData['other_parent_locations'] = $otherParentLocations;
539
                    }
540
                    break;
541
                case 'update':
542
                    $contentData = array_merge(
543
                        $contentData,
544
                        array(
545
                            'match' => array(
546
                                ContentMatcher::MATCH_CONTENT_REMOTE_ID => $content->contentInfo->remoteId
547
                            ),
548
                            'new_remote_id' => $content->contentInfo->remoteId,
549
                            'section' => $content->contentInfo->sectionId,
550
                            'object_states' => $this->getObjectStates($content),
551
                        )
552
                    );
553
                    break;
554
                case 'delete':
555
                    $contentData = array_merge(
556
                        $contentData,
557
                        array(
558
                            'match' => array(
559
                                ContentMatcher::MATCH_CONTENT_REMOTE_ID => $content->contentInfo->remoteId
560
                            )
561
                        )
562
                    );
563
                    break;
564
                default:
565
                    throw new \Exception("Executor 'content' doesn't support mode '$mode'");
566
            }
567
568
            if ($mode != 'delete') {
569
570
                $attributes = array();
571
                foreach ($content->getFieldsByLanguage($this->getLanguageCodeFromContext($context)) as $fieldIdentifier => $field) {
572
                    $fieldDefinition = $contentType->getFieldDefinition($fieldIdentifier);
573
                    $attributes[$field->fieldDefIdentifier] = $this->fieldHandlerManager->fieldValueToHash(
574
                        $fieldDefinition->fieldTypeIdentifier, $contentType->identifier, $field->value
575
                    );
576
                }
577
578
                $contentData = array_merge(
579
                    $contentData,
580
                    array(
581
                        'lang' => $this->getLanguageCodeFromContext($context),
582
                        'section' => $content->contentInfo->sectionId,
583
                        'owner' => $content->contentInfo->ownerId,
584
                        'modification_date' => $content->contentInfo->modificationDate->getTimestamp(),
585
                        'publication_date' => $content->contentInfo->publishedDate->getTimestamp(),
586
                        'always_available' => (bool)$content->contentInfo->alwaysAvailable,
587
                        'attributes' => $attributes
588
                    )
589
                );
590
            }
591
592
            $data[] = $contentData;
593
        }
594
595
        $this->loginUser($previousUserId);
596
        return $data;
597
    }
598
599
    /**
600
     * Helper function to set the fields of a ContentCreateStruct based on the DSL attribute settings.
601
     *
602
     * @param ContentCreateStruct|ContentUpdateStruct $createOrUpdateStruct
603
     * @param array $fields see description of expected format in code below
604
     * @param ContentType $contentType
605
     * @param $step
606
     * @throws \Exception
607
     */
608
    protected function setFields($createOrUpdateStruct, array $fields, ContentType $contentType, $step)
609
    {
610
        $fields = $this->normalizeFieldDefs($fields, $step);
611
612
        foreach ($fields as $fieldIdentifier => $fieldLanguages) {
613
            foreach ($fieldLanguages as $language => $fieldValue) {
614
                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...
615
                    throw new \Exception("Field '$fieldIdentifier' is not present in content type '{$contentType->identifier}'");
616
                }
617
618
                $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...
619
                $fieldValue = $this->getFieldValue($fieldValue, $fieldDefinition, $contentType->identifier, $step->context);
620
                $createOrUpdateStruct->setField($fieldIdentifier, $fieldValue, $language);
621
            }
622
        }
623
    }
624
625
    /**
626
     * Helper function to accommodate the definition of fields
627
     * - using a legacy DSL version
628
     * - using either single-language or multi-language style
629
     * 
630
     * @param array $fields
631
     * @return array
632
     */
633
    protected function normalizeFieldDefs($fields, $step)
634
    {
635
        $convertedFields = [];
636
        $i = 0;
637
        // the 'easy' yml: key = field name, value = value
638
        // deprecated: the 'legacy' yml: key = numerical index, value = array ( field name => value )
639
        foreach ($fields as $key => $field) {
640
            if ($key === $i && is_array($field) && count($field) == 1) {
641
                // each $field is one key value pair
642
                // eg.: $field = array($fieldIdentifier => $fieldValue)
643
                reset($field);
644
                $fieldIdentifier = key($field);
645
                $fieldValue = $field[$fieldIdentifier];
646
647
                $convertedFields[$fieldIdentifier] = $fieldValue;
648
            } else {
649
                $convertedFields[$key] = $field;
650
            }
651
            $i++;
652
        }
653
654
        // transform single-language field defs in multilang ones
655
        if (!$this->hasLanguageCodesAsKeys($convertedFields)) {
656
            $language = $this->getLanguageCode($step);
657
658
            foreach ($convertedFields as $fieldIdentifier => $fieldValue) {
659
                $convertedFields[$fieldIdentifier] = array($language => $fieldValue);
660
            }
661
        }
662
663
        return $convertedFields;
664
    }
665
666
    /**
667
     * Checks whether all fields are using multilang syntax ie. a valid language as key.
668
     *
669
     * @param array $fields
670
     * @return bool
671
     */
672
    protected function hasLanguageCodesAsKeys(array $fields)
673
    {
674
        $languageCodes = $this->getContentLanguageCodes();
675
676
        foreach ($fields as $fieldIdentifier => $fieldData) {
677
            if (!is_array($fieldData) || empty($fieldData)) {
678
                return false;
679
            }
680
681
            foreach ($fieldData as $key => $data) {
682
                if (!in_array($key, $languageCodes, true)) {
683
                    return false;
684
                }
685
            }
686
        }
687
688
        return true;
689
    }
690
691
    /**
692
     * Returns all enabled Languages in the repo.
693
     * @todo move to parent class?
694
     *
695
     * @return string[]
696
     */
697
    protected function getContentLanguageCodes()
698
    {
699
        return array_map(
700
            function($language) {
701
                return $language->languageCode;
702
            },
703
            array_filter(
704
                $this->repository->getContentLanguageService()->loadLanguages(),
705
                function ($language) {
706
                    return $language->enabled;
707
                }
708
            )
709
        );
710
    }
711
712 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...
713
    {
714
        $sectionKey = $this->referenceResolver->resolveReference($sectionKey);
715
        $section = $this->sectionMatcher->matchOneByKey($sectionKey);
716
717
        $sectionService = $this->repository->getSectionService();
718
        $sectionService->assignSection($content->contentInfo, $section);
719
    }
720
721
    protected function setObjectStates(Content $content, array $stateKeys)
722
    {
723
        foreach ($stateKeys as $stateKey) {
724
            $stateKey = $this->referenceResolver->resolveReference($stateKey);
725
            /** @var \eZ\Publish\API\Repository\Values\ObjectState\ObjectState $state */
726
            $state = $this->objectStateMatcher->matchOneByKey($stateKey);
727
728
            $stateService = $this->repository->getObjectStateService();
729
            $stateService->setContentState($content->contentInfo, $state->getObjectStateGroup(), $state);
730
        }
731
    }
732
733
    protected function setMainLocation(Content $content, $locationId)
734
    {
735
        $locationId = $this->referenceResolver->resolveReference($locationId);
736
        if (is_int($locationId) || ctype_digit($locationId)) {
737
            $location = $this->repository->getLocationService()->loadLocation($locationId);
738
        } else {
739
            $location = $this->repository->getLocationService()->loadLocationByRemoteId($locationId);
740
        }
741
742
        if ($location->contentInfo->id != $content->id) {
743
            throw new \Exception("Can not set main location {$location->id} to content {$content->id} as it belongs to another object");
744
        }
745
746
        $contentService = $this->repository->getContentService();
747
        $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
748
        $contentMetaDataUpdateStruct->mainLocationId = $location->id;
749
        $contentService->updateContentMetadata($location->contentInfo, $contentMetaDataUpdateStruct);
750
    }
751
752
    protected function getObjectStates(Content $content)
753
    {
754
        $states = [];
755
756
        $objectStateService = $this->repository->getObjectStateService();
757
        $groups = $objectStateService->loadObjectStateGroups();
758
        foreach ($groups as $group) {
759
            if (in_array($group->identifier, $this->ignoredStateGroupIdentifiers)) {
760
                continue;
761
            }
762
            $state = $objectStateService->getContentState($content->contentInfo, $group);
763
            $states[] = $group->identifier . '/' . $state->identifier;
764
        }
765
766
        return $states;
767
    }
768
769
    /**
770
     * Create the field value from the migration definition hash
771
     *
772
     * @param mixed $value
773
     * @param FieldDefinition $fieldDefinition
774
     * @param string $contentTypeIdentifier
775
     * @param array $context
776
     * @throws \InvalidArgumentException
777
     * @return mixed
778
     */
779
    protected function getFieldValue($value, FieldDefinition $fieldDefinition, $contentTypeIdentifier, array $context = array())
780
    {
781
        $fieldTypeIdentifier = $fieldDefinition->fieldTypeIdentifier;
782
783
        if (is_array($value) || $this->fieldHandlerManager->managesField($fieldTypeIdentifier, $contentTypeIdentifier)) {
784
            // since we now allow refs to be arrays, let's attempt a 1st pass at resolving them here instead of every single fieldHandler...
785
            if (is_string($value) && $this->fieldHandlerManager->doPreResolveStringReferences($fieldTypeIdentifier, $contentTypeIdentifier)) {
786
                $value = $this->referenceResolver->resolveReference($value);
787
            }
788
            // inject info about the current content type and field into the context
789
            $context['contentTypeIdentifier'] = $contentTypeIdentifier;
790
            $context['fieldIdentifier'] = $fieldDefinition->identifier;
791
            return $this->fieldHandlerManager->hashToFieldValue($fieldTypeIdentifier, $contentTypeIdentifier, $value, $context);
792
        }
793
794
        return $this->getSingleFieldValue($value, $fieldDefinition, $contentTypeIdentifier, $context);
795
    }
796
797
    /**
798
     * Create the field value for a primitive field from the migration definition hash
799
     *
800
     * @param mixed $value
801
     * @param FieldDefinition $fieldDefinition
802
     * @param string $contentTypeIdentifier
803
     * @param array $context
804
     * @throws \InvalidArgumentException
805
     * @return mixed
806
     */
807
    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...
808
    {
809
        // booleans were handled here. They are now handled as complextypes
810
811
        // q: do we really want this to happen by default on all scalar field values?
812
        // Note: if you want this *not* to happen, register a complex field for your scalar field...
813
        $value = $this->referenceResolver->resolveReference($value);
814
815
        return $value;
816
    }
817
818
    /**
819
     * Load user using either login, email, id - resolving eventual references
820
     * @param int|string $userKey
821
     * @return \eZ\Publish\API\Repository\Values\User\User
822
     */
823
    protected function getUser($userKey)
824
    {
825
        $userKey = $this->referenceResolver->resolveReference($userKey);
826
        return $this->userMatcher->matchOneByKey($userKey);
827
    }
828
829
    /**
830
     * @param int|string $date if integer, we assume a timestamp
831
     * @return \DateTime
832
     */
833
    protected function toDateTime($date)
834
    {
835
        if (is_int($date)) {
836
            return new \DateTime("@" . $date);
837
        } else {
838
            return new \DateTime($date);
839
        }
840
    }
841
}
842