Passed
Push — master ( 2b8f81...145d48 )
by Gaetano
10:22 queued 05:19
created

ContentManager::getContentLanguageCodes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 0
dl 0
loc 10
ccs 0
cts 10
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Core\Executor;
4
5
use eZ\Publish\API\Repository\Values\Content\Content;
6
use eZ\Publish\API\Repository\Values\Content\ContentCreateStruct;
7
use eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct;
8
use eZ\Publish\API\Repository\Values\Content\VersionInfo;
9
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
10
use eZ\Publish\API\Repository\Values\ContentType\FieldDefinition;
11
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
12
use Kaliop\eZMigrationBundle\API\Collection\ContentCollection;
13
use Kaliop\eZMigrationBundle\API\EnumerableMatcherInterface;
14
use Kaliop\eZMigrationBundle\API\Exception\InvalidStepDefinitionException;
15
use Kaliop\eZMigrationBundle\API\MigrationGeneratorInterface;
16
use Kaliop\eZMigrationBundle\Core\FieldHandlerManager;
17
use Kaliop\eZMigrationBundle\Core\Helper\SortConverter;
18
use Kaliop\eZMigrationBundle\Core\Matcher\ContentMatcher;
19
use Kaliop\eZMigrationBundle\Core\Matcher\ObjectStateGroupMatcher;
20
use Kaliop\eZMigrationBundle\Core\Matcher\ObjectStateMatcher;
21
use Kaliop\eZMigrationBundle\Core\Matcher\SectionMatcher;
22
use Kaliop\eZMigrationBundle\Core\Matcher\UserMatcher;
23
use JmesPath\Env as JmesPath;
24
25
/**
26
 * Handles content migrations.
27
 *
28
 * @todo add support for updating of content metadata
29
 */
30
class ContentManager extends RepositoryExecutor implements MigrationGeneratorInterface, EnumerableMatcherInterface
31
{
32
    protected $supportedStepTypes = array('content');
33
    protected $supportedActions = array('create', 'load', 'update', 'delete');
34
35
    protected $contentMatcher;
36
    protected $sectionMatcher;
37
    protected $userMatcher;
38
    protected $objectStateMatcher;
39
    protected $objectStateGroupMatcher;
40
    protected $fieldHandlerManager;
41
    protected $locationManager;
42
    protected $sortConverter;
43
44
    // these are not exported when generating a migration
45
    protected $ignoredStateGroupIdentifiers = array('ez_lock');
46
47
    public function __construct(
48
        ContentMatcher $contentMatcher,
49
        SectionMatcher $sectionMatcher,
50
        UserMatcher $userMatcher,
51
        ObjectStateMatcher $objectStateMatcher,
52
        ObjectStateGroupMatcher $objectStateGroupMatcher,
53
        FieldHandlerManager $fieldHandlerManager,
54
        LocationManager $locationManager,
55
        SortConverter $sortConverter
56
    ) {
57
        $this->contentMatcher = $contentMatcher;
58
        $this->sectionMatcher = $sectionMatcher;
59
        $this->userMatcher = $userMatcher;
60
        $this->objectStateMatcher = $objectStateMatcher;
61
        $this->objectStateGroupMatcher = $objectStateGroupMatcher;
62
        $this->fieldHandlerManager = $fieldHandlerManager;
63
        $this->locationManager = $locationManager;
64
        $this->sortConverter = $sortConverter;
65
    }
66
67
    /**
68
     * Handles the content create migration action type
69
     */
70
    protected function create($step)
71
    {
72
        $contentService = $this->repository->getContentService();
73
        $locationService = $this->repository->getLocationService();
74
        $contentTypeService = $this->repository->getContentTypeService();
75
76
        $contentTypeIdentifier = $step->dsl['content_type'];
77
        $contentTypeIdentifier = $this->referenceResolver->resolveReference($contentTypeIdentifier);
78
        /// @todo use a contenttypematcher
79
        $contentType = $contentTypeService->loadContentTypeByIdentifier($contentTypeIdentifier);
80
81
        $contentCreateStruct = $contentService->newContentCreateStruct($contentType, $this->getLanguageCode($step));
82
83
        $this->setFields($contentCreateStruct, $step->dsl['attributes'], $contentType, $step);
84
85
        if (isset($step->dsl['always_available'])) {
86
            $contentCreateStruct->alwaysAvailable = $step->dsl['always_available'];
87
        } else {
88
            // Could be removed when https://github.com/ezsystems/ezpublish-kernel/pull/1874 is merged,
89
            // but we strive to support old eZ kernel versions as well...
90
            $contentCreateStruct->alwaysAvailable = $contentType->defaultAlwaysAvailable;
91
        }
92
93
        if (isset($step->dsl['remote_id'])) {
94
            $contentCreateStruct->remoteId = $step->dsl['remote_id'];
95
        }
96
97
        if (isset($step->dsl['section'])) {
98
            $sectionKey = $this->referenceResolver->resolveReference($step->dsl['section']);
99
            $section = $this->sectionMatcher->matchOneByKey($sectionKey);
100
            $contentCreateStruct->sectionId = $section->id;
101
        }
102
103
        if (isset($step->dsl['owner'])) {
104
            $owner = $this->getUser($step->dsl['owner']);
105
            $contentCreateStruct->ownerId = $owner->id;
106
        }
107
108
        // This is a bit tricky, as the eZPublish API does not support having a different creator and owner with only 1 version.
109
        // We allow it, hoping that nothing gets broken because of it
110
        if (isset($step->dsl['version_creator'])) {
111
            $realContentOwnerId = $contentCreateStruct->ownerId;
112
            if ($realContentOwnerId == null) {
113
                $realContentOwnerId = $this->repository->getCurrentUser()->id;
114
            }
115
            $versionCreator = $this->getUser($step->dsl['version_creator']);
116
            $contentCreateStruct->ownerId = $versionCreator->id;
117
        }
118
119
        if (isset($step->dsl['modification_date'])) {
120
            $contentCreateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']);
121
        }
122
123
        // instantiate a location create struct from the parent location:
124
        // BC
125
        $locationId = isset($step->dsl['parent_location']) ? $step->dsl['parent_location'] : (
126
            isset($step->dsl['main_location']) ? $step->dsl['main_location'] : null
127
        );
128
        // 1st resolve references
129
        $locationId = $this->referenceResolver->resolveReference($locationId);
130
        // 2nd allow to specify the location via remote_id
131
        $locationId = $this->locationManager->matchLocationByKey($locationId)->id;
132
        $locationCreateStruct = $locationService->newLocationCreateStruct($locationId);
133
134
        if (isset($step->dsl['location_remote_id'])) {
135
            $locationCreateStruct->remoteId = $step->dsl['location_remote_id'];
136
        }
137
138
        if (isset($step->dsl['priority'])) {
139
            $locationCreateStruct->priority = $step->dsl['priority'];
140
        }
141
142
        if (isset($step->dsl['is_hidden'])) {
143
            $locationCreateStruct->hidden = $step->dsl['is_hidden'];
144
        }
145
146
        if (isset($step->dsl['sort_field'])) {
147
            $locationCreateStruct->sortField = $this->sortConverter->hash2SortField($step->dsl['sort_field']);
148
        } else {
149
            $locationCreateStruct->sortField = $contentType->defaultSortField;
150
        }
151
152
        if (isset($step->dsl['sort_order'])) {
153
            $locationCreateStruct->sortOrder = $this->sortConverter->hash2SortOrder($step->dsl['sort_order']);
154
        } else {
155
            $locationCreateStruct->sortOrder = $contentType->defaultSortOrder;
156
        }
157
158
        $locations = array($locationCreateStruct);
159
160
        // BC
161
        $other_locations = isset($step->dsl['other_parent_locations']) ? $step->dsl['other_parent_locations'] : (
162
            isset($step->dsl['other_locations']) ? $step->dsl['other_locations'] : null
163
        );
164
        if (isset($other_locations)) {
165
            foreach ($other_locations as $locationId) {
166
                $locationId = $this->referenceResolver->resolveReference($locationId);
167
                $locationId = $this->locationManager->matchLocationByKey($locationId)->id;
168
                $secondaryLocationCreateStruct = $locationService->newLocationCreateStruct($locationId);
169
                array_push($locations, $secondaryLocationCreateStruct);
170
            }
171
        }
172
173
        // create a draft using the content and location create struct and publish it
174
        $draft = $contentService->createContent($contentCreateStruct, $locations);
175
        $content = $contentService->publishVersion($draft->versionInfo);
176
177
        if (isset($step->dsl['object_states'])) {
178
            $this->setObjectStates($content, $step->dsl['object_states']);
179
        }
180
181
        // 2nd part of the hack: re-set the content owner to its intended value
182
        if (isset($step->dsl['version_creator']) || isset($step->dsl['publication_date'])) {
183
            $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
184
185
            if (isset($step->dsl['version_creator'])) {
186
                $contentMetaDataUpdateStruct->ownerId = $realContentOwnerId;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $realContentOwnerId does not seem to be defined for all execution paths leading up to this point.
Loading history...
187
            }
188
            if (isset($step->dsl['publication_date'])) {
189
                $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($step->dsl['publication_date']);
190
            }
191
            // we have to do this to make sure we preserve the custom modification date
192
            if (isset($step->dsl['modification_date'])) {
193
                $contentMetaDataUpdateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']);
194
            }
195
196
            $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct);
197
        }
198
199
        $this->setReferences($content, $step);
200
201
        return $content;
202
    }
203
204
    protected function load($step)
205
    {
206
        $contentCollection = $this->matchContents('load', $step);
207
208
        $this->validateResultsCount($contentCollection, $step);
209
210
        $this->setReferences($contentCollection, $step);
211
212
        return $contentCollection;
213
    }
214
215
    /**
216
     * Handles the content update migration action type
217
     *
218
     * @todo handle updating of more metadata fields
219
     */
220
    protected function update($step)
221
    {
222
        $contentService = $this->repository->getContentService();
223
        $contentTypeService = $this->repository->getContentTypeService();
224
225
        $contentCollection = $this->matchContents('update', $step);
226
227
        $this->validateResultsCount($contentCollection, $step);
228
229
        if (count($contentCollection) > 1 && isset($step->dsl['main_location'])) {
230
            throw new \Exception("Can not execute Content update because multiple contents match, and a main_location section is specified in the dsl.");
231
        }
232
233
        $contentType = array();
234
235
        foreach ($contentCollection as $key => $content) {
236
            $contentInfo = $content->contentInfo;
237
238
            if (!isset($contentType[$contentInfo->contentTypeId])) {
239
                $contentType[$contentInfo->contentTypeId] = $contentTypeService->loadContentType($contentInfo->contentTypeId);
240
            }
241
242
            if (isset($step->dsl['attributes']) || isset($step->dsl['version_creator'])) {
243
                $contentUpdateStruct = $contentService->newContentUpdateStruct();
244
245
                if (isset($step->dsl['attributes'])) {
246
                    $this->setFields($contentUpdateStruct, $step->dsl['attributes'], $contentType[$contentInfo->contentTypeId], $step);
247
                }
248
249
                $versionCreator = null;
250
                if (isset($step->dsl['version_creator'])) {
251
                    $versionCreator = $this->getUser($step->dsl['version_creator']);
252
                }
253
254
                $draft = $contentService->createContentDraft($contentInfo, null, $versionCreator);
255
                $contentService->updateContent($draft->versionInfo, $contentUpdateStruct);
256
                $content = $contentService->publishVersion($draft->versionInfo);
257
            }
258
259
            if (isset($step->dsl['always_available']) ||
260
                isset($step->dsl['new_remote_id']) ||
261
                isset($step->dsl['owner']) ||
262
                isset($step->dsl['modification_date']) ||
263
                isset($step->dsl['publication_date'])) {
264
265
                $contentMetaDataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
266
267
                if (isset($step->dsl['always_available'])) {
268
                    $contentMetaDataUpdateStruct->alwaysAvailable = $step->dsl['always_available'];
269
                }
270
271
                if (isset($step->dsl['new_remote_id'])) {
272
                    $contentMetaDataUpdateStruct->remoteId = $step->dsl['new_remote_id'];
273
                }
274
275
                if (isset($step->dsl['owner'])) {
276
                    $owner = $this->getUser($step->dsl['owner']);
277
                    $contentMetaDataUpdateStruct->ownerId = $owner->id;
278
                }
279
280
                if (isset($step->dsl['modification_date'])) {
281
                    $contentMetaDataUpdateStruct->modificationDate = $this->toDateTime($step->dsl['modification_date']);
282
                }
283
284
                if (isset($step->dsl['publication_date'])) {
285
                    $contentMetaDataUpdateStruct->publishedDate = $this->toDateTime($step->dsl['publication_date']);
286
                }
287
288
                $content = $contentService->updateContentMetadata($content->contentInfo, $contentMetaDataUpdateStruct);
289
            }
290
291
            if (isset($step->dsl['section'])) {
292
                $this->setSection($content, $step->dsl['section']);
293
            }
294
295
            if (isset($step->dsl['object_states'])) {
296
                $this->setObjectStates($content, $step->dsl['object_states']);
297
            }
298
299
            if (isset($step->dsl['main_location'])) {
300
                $this->setMainLocation($content, $step->dsl['main_location']);
301
302
            }
303
            $contentCollection[$key] = $content;
304
        }
305
306
        $this->setReferences($contentCollection, $step);
307
308
        return $contentCollection;
309
    }
310
311
    /**
312
     * Handles the content delete migration action type
313
     */
314
    protected function delete($step)
315
    {
316
        $contentCollection = $this->matchContents('delete', $step);
317
318
        $this->validateResultsCount($contentCollection, $step);
319
320
        $this->setReferences($contentCollection, $step);
321
322
        $contentService = $this->repository->getContentService();
323
324
        foreach ($contentCollection as $content) {
325
            try {
326
                $contentService->deleteContent($content->contentInfo);
327
            } catch (NotFoundException $e) {
328
                // Someone else (or even us, by virtue of location tree?) removed the content which we found just a
329
                // second ago. We can safely ignore this
330
            }
331
        }
332
333
        return $contentCollection;
334
    }
335
336
    /**
337
     * @param string $action
338
     * @return ContentCollection
339
     * @throws \Exception
340
     */
341
    protected function matchContents($action, $step)
342
    {
343
        if (!isset($step->dsl['object_id']) && !isset($step->dsl['remote_id']) && !isset($step->dsl['match'])) {
344
            throw new InvalidStepDefinitionException("The id or remote id of an object or a match condition is required to $action a content");
345
        }
346
347
        // Backwards compat
348
349
        if (isset($step->dsl['match'])) {
350
            $match = $step->dsl['match'];
351
        } else {
352
            if (isset($step->dsl['object_id'])) {
353
                $match = array('content_id' => $step->dsl['object_id']);
354
            } elseif (isset($step->dsl['remote_id'])) {
355
                $match = array('content_remote_id' => $step->dsl['remote_id']);
356
            }
357
        }
358
359
        // convert the references passed in the match
360
        $match = $this->resolveReferencesRecursively($match);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $match does not seem to be defined for all execution paths leading up to this point.
Loading history...
361
362
        $offset = isset($step->dsl['match_offset']) ? $this->referenceResolver->resolveReference($step->dsl['match_offset']) : 0;
363
        $limit = isset($step->dsl['match_limit']) ? $this->referenceResolver->resolveReference($step->dsl['match_limit']) : 0;
364
        $sort = isset($step->dsl['match_sort']) ? $this->referenceResolver->resolveReference($step->dsl['match_sort']) : array();
365
366
        $tolerateMisses = isset($step->dsl['match_tolerate_misses']) ? $this->referenceResolver->resolveReference($step->dsl['match_tolerate_misses']) : false;
367
368
        return $this->contentMatcher->match($match, $sort, $offset, $limit, $tolerateMisses);
369
    }
370
371
    /**
372
     * @param Content|VersionInfo $content
373
     * @param array $references the definitions of the references to set
374
     * @throws \InvalidArgumentException When trying to assign a reference to an unsupported attribute
375
     * @throws InvalidStepDefinitionException
376
     * @return array key: the reference names, values: the reference values
377
     */
378
    protected function getReferencesValues($content, array $references, $step)
379
    {
380
        $refs = array();
381
382
        foreach ($references as $key => $reference) {
383
384
            $reference = $this->parseReferenceDefinition($key, $reference);
385
386
            switch ($reference['attribute']) {
387
                case 'object_id':
388
                case 'content_id':
389
                case 'id':
390
                    $value = $content->id;
391
                    break;
392
                case 'remote_id':
393
                case 'content_remote_id':
394
                    $value = $content->contentInfo->remoteId;
395
                    break;
396
                case 'always_available':
397
                    $value = $content->contentInfo->alwaysAvailable;
398
                    break;
399
                case 'content_type_id':
400
                    $value = $content->contentInfo->contentTypeId;
401
                    break;
402
                case 'content_type_identifier':
403
                    $contentTypeService = $this->repository->getContentTypeService();
404
                    $value = $contentTypeService->loadContentType($content->contentInfo->contentTypeId)->identifier;
405
                    break;
406
                case 'current_version':
407
                case 'current_version_no':
408
                    $value = $content->contentInfo->currentVersionNo;
409
                    break;
410
                case 'location_id':
411
                case 'main_location_id':
412
                    $value = $content->contentInfo->mainLocationId;
413
                    break;
414
                case 'location_remote_id':
415
                    $locationService = $this->repository->getLocationService();
416
                    $value = $locationService->loadLocation($content->contentInfo->mainLocationId)->remoteId;
417
                    break;
418
                case 'main_language_code':
419
                    $value = $content->contentInfo->mainLanguageCode;
420
                    break;
421
                case 'modification_date':
422
                    $value = $content->contentInfo->modificationDate->getTimestamp();
423
                    break;
424
                case 'name':
425
                    $value = $content->contentInfo->name;
426
                    break;
427
                case 'owner_id':
428
                    $value = $content->contentInfo->ownerId;
429
                    break;
430
                case 'path':
431
                    $locationService = $this->repository->getLocationService();
432
                    $value = $locationService->loadLocation($content->contentInfo->mainLocationId)->pathString;
433
                    break;
434
                case 'publication_date':
435
                    $value = $content->contentInfo->publishedDate->getTimestamp();
436
                    break;
437
                case 'section_id':
438
                    $value = $content->contentInfo->sectionId;
439
                    break;
440
                case 'section_identifier':
441
                    $sectionService = $this->repository->getSectionService();
442
                    $value = $sectionService->loadSection($content->contentInfo->sectionId)->identifier;
443
                    break;
444
                case 'version_count':
445
                    $contentService = $this->repository->getContentService();
446
                    $value = count($contentService->loadVersions($content->contentInfo));
447
                    break;
448
                default:
449
                    if (strpos($reference['attribute'], 'object_state.') === 0) {
450
                        $stateGroupKey = substr($reference['attribute'], 13);
451
                        $stateGroup = $this->objectStateGroupMatcher->matchOneByKey($stateGroupKey);
452
                        $value = $stateGroupKey . '/' . $this->repository->getObjectStateService()->
453
                            getContentState($content->contentInfo, $stateGroup)->identifier;
454
                        break;
455
                    }
456
457
                    // allow to get the value of fields as well as their sub-parts
458
                    if (strpos($reference['attribute'], 'attributes.') === 0) {
459
                        $contentType = $this->repository->getContentTypeService()->loadContentType(
460
                            $content->contentInfo->contentTypeId
461
                        );
462
                        $parts = explode('.', $reference['attribute']);
463
                        // totally not sure if this list of special chars is correct for what could follow a jmespath identifier...
464
                        // also what about quoted strings?
465
                        $fieldIdentifier = preg_replace('/[[(|&!{].*$/', '', $parts[1]);
466
                        $field = $content->getField($fieldIdentifier);
0 ignored issues
show
Bug introduced by
The method getField() does not exist on eZ\Publish\API\Repositor...ues\Content\VersionInfo. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

466
                        /** @scrutinizer ignore-call */ 
467
                        $field = $content->getField($fieldIdentifier);

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

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

Loading history...
Bug introduced by
The method getField() does not exist on eZ\Publish\API\Repository\Values\Content\Content. Did you maybe mean getFieldValue()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

466
                        /** @scrutinizer ignore-call */ 
467
                        $field = $content->getField($fieldIdentifier);

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

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

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

855
    protected function getSingleFieldValue($value, /** @scrutinizer ignore-unused */ FieldDefinition $fieldDefinition, $contentTypeIdentifier, array $context = array())

This check looks for 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. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

855
    protected function getSingleFieldValue($value, FieldDefinition $fieldDefinition, /** @scrutinizer ignore-unused */ $contentTypeIdentifier, array $context = array())

This check looks for 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. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

855
    protected function getSingleFieldValue($value, FieldDefinition $fieldDefinition, $contentTypeIdentifier, /** @scrutinizer ignore-unused */ array $context = array())

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
856
    {
857
        // booleans were handled here. They are now handled as complextypes
858
859
        // q: do we really want this to happen by default on all scalar field values?
860
        // Note: if you want this *not* to happen, register a complex field for your scalar field...
861
        $value = $this->referenceResolver->resolveReference($value);
862
863
        return $value;
864
    }
865
866
    /**
867
     * Load user using either login, email, id - resolving eventual references
868
     * @param int|string $userKey
869
     * @return \eZ\Publish\API\Repository\Values\User\User
870
     */
871
    protected function getUser($userKey)
872
    {
873
        $userKey = $this->referenceResolver->resolveReference($userKey);
874
        return $this->userMatcher->matchOneByKey($userKey);
875
    }
876
877
    /**
878
     * @param int|string $date if integer, we assume a timestamp
879
     * @return \DateTime
880
     */
881
    protected function toDateTime($date)
882
    {
883
        if (is_int($date)) {
884
            return new \DateTime("@" . $date);
885
        } else {
886
            return new \DateTime($date);
887
        }
888
    }
889
}
890