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

ContentManager::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 8
dl 0
loc 18
ccs 0
cts 18
cp 0
crap 2
rs 10
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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