Completed
Push — master ( ac4d76...6059dd )
by
unknown
15:13 queued 12s
created

buildContentDomainObjectFromPersistence()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 4
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * File containing the DomainMapper class.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 */
9
namespace eZ\Publish\Core\Repository\Helper;
10
11
use eZ\Publish\API\Repository\Values\Content\Search\SearchResult;
12
use eZ\Publish\API\Repository\Values\Content\Content as APIContent;
13
use eZ\Publish\Core\FieldType\FieldTypeRegistry;
14
use eZ\Publish\SPI\Persistence\Content\Handler as ContentHandler;
15
use eZ\Publish\SPI\Persistence\Content\Location\Handler as LocationHandler;
16
use eZ\Publish\SPI\Persistence\Content\Language\Handler as LanguageHandler;
17
use eZ\Publish\SPI\Persistence\Content\Type\Handler as TypeHandler;
18
use eZ\Publish\Core\Repository\Values\Content\Content;
19
use eZ\Publish\Core\Repository\Values\Content\ContentProxy;
20
use eZ\Publish\API\Repository\Values\Content\VersionInfo as APIVersionInfo;
21
use eZ\Publish\Core\Repository\Values\Content\VersionInfo;
22
use eZ\Publish\API\Repository\Values\Content\ContentInfo;
23
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
24
use eZ\Publish\API\Repository\Values\Content\Field;
25
use eZ\Publish\Core\Repository\Values\Content\Relation;
26
use eZ\Publish\API\Repository\Values\Content\Location as APILocation;
27
use eZ\Publish\Core\Repository\Values\Content\Location;
28
use eZ\Publish\SPI\Persistence\Content as SPIContent;
29
use eZ\Publish\SPI\Persistence\Content\Location as SPILocation;
30
use eZ\Publish\SPI\Persistence\Content\VersionInfo as SPIVersionInfo;
31
use eZ\Publish\SPI\Persistence\Content\ContentInfo as SPIContentInfo;
32
use eZ\Publish\SPI\Persistence\Content\Relation as SPIRelation;
33
use eZ\Publish\SPI\Persistence\Content\Type as SPIContentType;
34
use eZ\Publish\SPI\Persistence\Content\Location\CreateStruct as SPILocationCreateStruct;
35
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
36
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
37
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue;
38
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentType;
39
use DateTime;
40
41
/**
42
 * DomainMapper is an internal service.
43
 *
44
 * @internal Meant for internal use by Repository.
45
 */
46
class DomainMapper
47
{
48
    const MAX_LOCATION_PRIORITY = 2147483647;
49
    const MIN_LOCATION_PRIORITY = -2147483648;
50
51
    /** @var \eZ\Publish\SPI\Persistence\Content\Handler */
52
    protected $contentHandler;
53
54
    /** @var \eZ\Publish\SPI\Persistence\Content\Location\Handler */
55
    protected $locationHandler;
56
57
    /** @var \eZ\Publish\SPI\Persistence\Content\Type\Handler */
58
    protected $contentTypeHandler;
59
60
    /** @var \eZ\Publish\Core\Repository\Helper\ContentTypeDomainMapper */
61
    protected $contentTypeDomainMapper;
62
63
    /** @var \eZ\Publish\SPI\Persistence\Content\Language\Handler */
64
    protected $contentLanguageHandler;
65
66
    /** @var \eZ\Publish\Core\Repository\Helper\FieldTypeRegistry */
67
    protected $fieldTypeRegistry;
68
69
    /**
70
     * Setups service with reference to repository.
71
     *
72
     * @param \eZ\Publish\SPI\Persistence\Content\Handler $contentHandler
73
     * @param \eZ\Publish\SPI\Persistence\Content\Location\Handler $locationHandler
74
     * @param \eZ\Publish\SPI\Persistence\Content\Type\Handler $contentTypeHandler
75
     * @param \eZ\Publish\Core\Repository\Helper\ContentTypeDomainMapper $contentTypeDomainMapper
76
     * @param \eZ\Publish\SPI\Persistence\Content\Language\Handler $contentLanguageHandler
77
     * @param \eZ\Publish\Core\FieldType\FieldTypeRegistry $fieldTypeRegistry
78
     */
79
    public function __construct(
80
        ContentHandler $contentHandler,
81
        LocationHandler $locationHandler,
82
        TypeHandler $contentTypeHandler,
83
        ContentTypeDomainMapper $contentTypeDomainMapper,
84
        LanguageHandler $contentLanguageHandler,
85
        FieldTypeRegistry $fieldTypeRegistry
86
    ) {
87
        $this->contentHandler = $contentHandler;
88
        $this->locationHandler = $locationHandler;
89
        $this->contentTypeHandler = $contentTypeHandler;
90
        $this->contentTypeDomainMapper = $contentTypeDomainMapper;
91
        $this->contentLanguageHandler = $contentLanguageHandler;
92
        $this->fieldTypeRegistry = $fieldTypeRegistry;
0 ignored issues
show
Documentation Bug introduced by
It seems like $fieldTypeRegistry of type object<eZ\Publish\Core\F...Type\FieldTypeRegistry> is incompatible with the declared type object<eZ\Publish\Core\R...lper\FieldTypeRegistry> of property $fieldTypeRegistry.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
93
    }
94
95
    /**
96
     * Builds a Content domain object from value object.
97
     *
98
     * @param \eZ\Publish\SPI\Persistence\Content $spiContent
99
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
100
     * @param array $prioritizedLanguages Prioritized language codes to filter fields on
101
     * @param string|null $fieldAlwaysAvailableLanguage Language code fallback if a given field is not found in $prioritizedLanguages
102
     *
103
     * @return \eZ\Publish\Core\Repository\Values\Content\Content
104
     */
105
    public function buildContentDomainObject(
106
        SPIContent $spiContent,
107
        ContentType $contentType,
108
        array $prioritizedLanguages = [],
109
        string $fieldAlwaysAvailableLanguage = null
110
    ) {
111
        $prioritizedFieldLanguageCode = null;
112
        if (!empty($prioritizedLanguages)) {
113
            $availableFieldLanguageMap = array_fill_keys($spiContent->versionInfo->languageCodes, true);
114
            foreach ($prioritizedLanguages as $prioritizedLanguage) {
115
                if (isset($availableFieldLanguageMap[$prioritizedLanguage])) {
116
                    $prioritizedFieldLanguageCode = $prioritizedLanguage;
117
                    break;
118
                }
119
            }
120
        }
121
122
        return new Content(
123
            [
124
                'internalFields' => $this->buildDomainFields($spiContent->fields, $contentType, $prioritizedLanguages, $fieldAlwaysAvailableLanguage),
125
                'versionInfo' => $this->buildVersionInfoDomainObject($spiContent->versionInfo, $prioritizedLanguages),
126
                'contentType' => $contentType,
127
                'prioritizedFieldLanguageCode' => $prioritizedFieldLanguageCode,
128
            ]
129
        );
130
    }
131
132
    /**
133
     * Builds a Content domain object from value object returned from persistence.
134
     *
135
     * @param \eZ\Publish\SPI\Persistence\Content $spiContent
136
     * @param \eZ\Publish\SPI\Persistence\Content\Type $spiContentType
137
     * @param string[] $prioritizedLanguages Prioritized language codes to filter fields on
138
     * @param string|null $fieldAlwaysAvailableLanguage Language code fallback if a given field is not found in $prioritizedLanguages
139
     *
140
     * @return \eZ\Publish\Core\Repository\Values\Content\Content
141
     */
142
    public function buildContentDomainObjectFromPersistence(
143
        SPIContent $spiContent,
144
        SPIContentType $spiContentType,
145
        array $prioritizedLanguages = [],
146
        ?string $fieldAlwaysAvailableLanguage = null
147
    ): APIContent {
148
        $contentType = $this->contentTypeDomainMapper->buildContentTypeDomainObject($spiContentType, $prioritizedLanguages);
149
150
        return $this->buildContentDomainObject($spiContent, $contentType, $prioritizedLanguages, $fieldAlwaysAvailableLanguage);
151
    }
152
153
    /**
154
     * Builds a Content proxy object (lazy loaded, loads as soon as used).
155
     */
156
    public function buildContentProxy(
157
        SPIContent\ContentInfo $info,
158
        array $prioritizedLanguages = [],
159
        bool $useAlwaysAvailable = true
160
    ): APIContent {
161
        $generator = $this->generatorForContentList([$info], $prioritizedLanguages, $useAlwaysAvailable);
162
163
        return new ContentProxy($generator, $info->id);
164
    }
165
166
    /**
167
     * Builds a list of Content proxy objects (lazy loaded, loads all as soon as one of them loads).
168
     *
169
     * @param \eZ\Publish\SPI\Persistence\Content\ContentInfo[] $infoList
170
     * @param string[] $prioritizedLanguages
171
     * @param bool $useAlwaysAvailable
172
     *
173
     * @return \eZ\Publish\API\Repository\Values\Content\Content[<int>]
0 ignored issues
show
Documentation introduced by
The doc-type \eZ\Publish\API\Reposito...\Content\Content[<int>] could not be parsed: Expected "]" at position 2, but found "<". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
174
     */
175
    public function buildContentProxyList(
176
        array $infoList,
177
        array $prioritizedLanguages = [],
178
        bool $useAlwaysAvailable = true
179
    ): array {
180
        $list = [];
181
        $generator = $this->generatorForContentList($infoList, $prioritizedLanguages, $useAlwaysAvailable);
182
        foreach ($infoList as $info) {
183
            $list[$info->id] = new ContentProxy($generator, $info->id);
184
        }
185
186
        return $list;
187
    }
188
189
    /**
190
     * @todo Maybe change signature to generatorForContentList($contentIds, $prioritizedLanguages, $translations)
191
     * @todo to avoid keeping referance to $infoList all the way until the generator is called.
192
     *
193
     * @param \eZ\Publish\SPI\Persistence\Content\ContentInfo[] $infoList
194
     * @param string[] $prioritizedLanguages
195
     * @param bool $useAlwaysAvailable
196
     *
197
     * @return \Generator
198
     */
199
    private function generatorForContentList(
200
        array $infoList,
201
        array $prioritizedLanguages = [],
202
        bool $useAlwaysAvailable = true
203
    ): \Generator {
204
        $contentIds = [];
205
        $contentTypeIds = [];
206
        $translations = $prioritizedLanguages;
207
        foreach ($infoList as $info) {
208
            $contentIds[] = $info->id;
209
            $contentTypeIds[] = $info->contentTypeId;
210
            // Unless we are told to load all languages, we add main language to translations so they are loaded too
211
            // Might in some case load more languages then intended, but prioritised handling will pick right one
212
            if (!empty($prioritizedLanguages) && $useAlwaysAvailable && $info->alwaysAvailable) {
213
                $translations[] = $info->mainLanguageCode;
214
            }
215
        }
216
217
        unset($infoList);
218
219
        $contentList = $this->contentHandler->loadContentList($contentIds, array_unique($translations));
220
        $contentTypeList = $this->contentTypeHandler->loadContentTypeList(array_unique($contentTypeIds));
221
        while (!empty($contentList)) {
222
            $id = yield;
223
            /** @var \eZ\Publish\SPI\Persistence\Content\ContentInfo $info */
224
            $info = $contentList[$id]->versionInfo->contentInfo;
225
            yield $this->buildContentDomainObject(
226
                $contentList[$id],
227
                $this->contentTypeDomainMapper->buildContentTypeDomainObject(
228
                    $contentTypeList[$info->contentTypeId],
229
                    $prioritizedLanguages
230
                ),
231
                $prioritizedLanguages,
232
                $info->alwaysAvailable ? $info->mainLanguageCode : null
233
            );
234
235
            unset($contentList[$id]);
236
        }
237
    }
238
239
    /**
240
     * Returns an array of domain fields created from given array of SPI fields.
241
     *
242
     * @throws InvalidArgumentType On invalid $contentType
243
     *
244
     * @param \eZ\Publish\SPI\Persistence\Content\Field[] $spiFields
245
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType|\eZ\Publish\SPI\Persistence\Content\Type $contentType
246
     * @param array $prioritizedLanguages A language priority, filters returned fields and is used as prioritized language code on
247
     *                         returned value object. If not given all languages are returned.
248
     * @param string|null $alwaysAvailableLanguage Language code fallback if a given field is not found in $prioritizedLanguages
249
     *
250
     * @return array
251
     */
252
    public function buildDomainFields(
253
        array $spiFields,
254
        $contentType,
255
        array $prioritizedLanguages = [],
256
        string $alwaysAvailableLanguage = null
257
    ) {
258
        if (!$contentType instanceof SPIContentType && !$contentType instanceof ContentType) {
259
            throw new InvalidArgumentType('$contentType', 'SPI ContentType | API ContentType');
260
        }
261
262
        $fieldDefinitionsMap = [];
263
        foreach ($contentType->fieldDefinitions as $fieldDefinition) {
264
            $fieldDefinitionsMap[$fieldDefinition->id] = $fieldDefinition;
265
        }
266
267
        $fieldInFilterLanguagesMap = [];
268
        if (!empty($prioritizedLanguages) && $alwaysAvailableLanguage !== null) {
269
            foreach ($spiFields as $spiField) {
270
                if (in_array($spiField->languageCode, $prioritizedLanguages)) {
271
                    $fieldInFilterLanguagesMap[$spiField->fieldDefinitionId] = true;
272
                }
273
            }
274
        }
275
276
        $fields = [];
277
        foreach ($spiFields as $spiField) {
278
            // We ignore fields in content not part of the content type
279
            if (!isset($fieldDefinitionsMap[$spiField->fieldDefinitionId])) {
280
                continue;
281
            }
282
283
            $fieldDefinition = $fieldDefinitionsMap[$spiField->fieldDefinitionId];
284
285
            if (!empty($prioritizedLanguages) && !in_array($spiField->languageCode, $prioritizedLanguages)) {
286
                // If filtering is enabled we ignore fields in other languages then $prioritizedLanguages, if:
287
                if ($alwaysAvailableLanguage === null) {
288
                    // Ignore field if we don't have $alwaysAvailableLanguageCode fallback
289
                    continue;
290
                } elseif (!empty($fieldInFilterLanguagesMap[$spiField->fieldDefinitionId])) {
291
                    // Ignore field if it exists in one of the filtered languages
292
                    continue;
293
                } elseif ($spiField->languageCode !== $alwaysAvailableLanguage) {
294
                    // Also ignore if field is not in $alwaysAvailableLanguageCode
295
                    continue;
296
                }
297
            }
298
299
            $fields[$fieldDefinition->position][] = new Field(
300
                [
301
                    'id' => $spiField->id,
302
                    'value' => $this->fieldTypeRegistry->getFieldType($spiField->type)
303
                        ->fromPersistenceValue($spiField->value),
304
                    'languageCode' => $spiField->languageCode,
305
                    'fieldDefIdentifier' => $fieldDefinition->identifier,
306
                    'fieldTypeIdentifier' => $spiField->type,
307
                ]
308
            );
309
        }
310
311
        // Sort fields by content type field definition priority
312
        ksort($fields, SORT_NUMERIC);
313
314
        // Flatten array
315
        return array_merge(...$fields);
316
    }
317
318
    /**
319
     * Builds a VersionInfo domain object from value object returned from persistence.
320
     *
321
     * @param \eZ\Publish\SPI\Persistence\Content\VersionInfo $spiVersionInfo
322
     * @param array $prioritizedLanguages
323
     *
324
     * @return \eZ\Publish\Core\Repository\Values\Content\VersionInfo
325
     */
326
    public function buildVersionInfoDomainObject(SPIVersionInfo $spiVersionInfo, array $prioritizedLanguages = [])
327
    {
328
        // Map SPI statuses to API
329
        switch ($spiVersionInfo->status) {
330
            case SPIVersionInfo::STATUS_ARCHIVED:
331
                $status = APIVersionInfo::STATUS_ARCHIVED;
332
                break;
333
334
            case SPIVersionInfo::STATUS_PUBLISHED:
335
                $status = APIVersionInfo::STATUS_PUBLISHED;
336
                break;
337
338
            case SPIVersionInfo::STATUS_DRAFT:
339
            default:
340
                $status = APIVersionInfo::STATUS_DRAFT;
341
        }
342
343
        // Find prioritised language among names
344
        $prioritizedNameLanguageCode = null;
345
        foreach ($prioritizedLanguages as $prioritizedLanguage) {
346
            if (isset($spiVersionInfo->names[$prioritizedLanguage])) {
347
                $prioritizedNameLanguageCode = $prioritizedLanguage;
348
                break;
349
            }
350
        }
351
352
        return new VersionInfo(
353
            [
354
                'id' => $spiVersionInfo->id,
355
                'versionNo' => $spiVersionInfo->versionNo,
356
                'modificationDate' => $this->getDateTime($spiVersionInfo->modificationDate),
357
                'creatorId' => $spiVersionInfo->creatorId,
358
                'creationDate' => $this->getDateTime($spiVersionInfo->creationDate),
359
                'status' => $status,
360
                'initialLanguageCode' => $spiVersionInfo->initialLanguageCode,
361
                'languageCodes' => $spiVersionInfo->languageCodes,
362
                'names' => $spiVersionInfo->names,
363
                'contentInfo' => $this->buildContentInfoDomainObject($spiVersionInfo->contentInfo),
364
                'prioritizedNameLanguageCode' => $prioritizedNameLanguageCode,
365
            ]
366
        );
367
    }
368
369
    /**
370
     * Builds a ContentInfo domain object from value object returned from persistence.
371
     *
372
     * @param \eZ\Publish\SPI\Persistence\Content\ContentInfo $spiContentInfo
373
     *
374
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
375
     */
376
    public function buildContentInfoDomainObject(SPIContentInfo $spiContentInfo)
377
    {
378
        // Map SPI statuses to API
379
        switch ($spiContentInfo->status) {
380
            case SPIContentInfo::STATUS_TRASHED:
381
                $status = ContentInfo::STATUS_TRASHED;
382
                break;
383
384
            case SPIContentInfo::STATUS_PUBLISHED:
385
                $status = ContentInfo::STATUS_PUBLISHED;
386
                break;
387
388
            case SPIContentInfo::STATUS_DRAFT:
389
            default:
390
                $status = ContentInfo::STATUS_DRAFT;
391
        }
392
393
        return new ContentInfo(
394
            [
395
                'id' => $spiContentInfo->id,
396
                'contentTypeId' => $spiContentInfo->contentTypeId,
397
                'name' => $spiContentInfo->name,
398
                'sectionId' => $spiContentInfo->sectionId,
399
                'currentVersionNo' => $spiContentInfo->currentVersionNo,
400
                'published' => $spiContentInfo->isPublished,
401
                'ownerId' => $spiContentInfo->ownerId,
402
                'modificationDate' => $spiContentInfo->modificationDate == 0 ?
403
                    null :
404
                    $this->getDateTime($spiContentInfo->modificationDate),
405
                'publishedDate' => $spiContentInfo->publicationDate == 0 ?
406
                    null :
407
                    $this->getDateTime($spiContentInfo->publicationDate),
408
                'alwaysAvailable' => $spiContentInfo->alwaysAvailable,
409
                'remoteId' => $spiContentInfo->remoteId,
410
                'mainLanguageCode' => $spiContentInfo->mainLanguageCode,
411
                'mainLocationId' => $spiContentInfo->mainLocationId,
412
                'status' => $status,
413
                'isHidden' => $spiContentInfo->isHidden,
414
            ]
415
        );
416
    }
417
418
    /**
419
     * Builds API Relation object from provided SPI Relation object.
420
     *
421
     * @param \eZ\Publish\SPI\Persistence\Content\Relation $spiRelation
422
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $sourceContentInfo
423
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $destinationContentInfo
424
     *
425
     * @return \eZ\Publish\API\Repository\Values\Content\Relation
426
     */
427
    public function buildRelationDomainObject(
428
        SPIRelation $spiRelation,
429
        ContentInfo $sourceContentInfo,
430
        ContentInfo $destinationContentInfo
431
    ) {
432
        $sourceFieldDefinitionIdentifier = null;
433
        if ($spiRelation->sourceFieldDefinitionId !== null) {
434
            $contentType = $this->contentTypeHandler->load($sourceContentInfo->contentTypeId);
435
            foreach ($contentType->fieldDefinitions as $fieldDefinition) {
436
                if ($fieldDefinition->id !== $spiRelation->sourceFieldDefinitionId) {
437
                    continue;
438
                }
439
440
                $sourceFieldDefinitionIdentifier = $fieldDefinition->identifier;
441
                break;
442
            }
443
        }
444
445
        return new Relation(
446
            [
447
                'id' => $spiRelation->id,
448
                'sourceFieldDefinitionIdentifier' => $sourceFieldDefinitionIdentifier,
449
                'type' => $spiRelation->type,
450
                'sourceContentInfo' => $sourceContentInfo,
451
                'destinationContentInfo' => $destinationContentInfo,
452
            ]
453
        );
454
    }
455
456
    /**
457
     * @deprecated Since 7.2, use buildLocationWithContent(), buildLocation() or (private) mapLocation() instead.
458
     */
459
    public function buildLocationDomainObject(
460
        SPILocation $spiLocation,
461
        SPIContentInfo $contentInfo = null
462
    ) {
463
        if ($contentInfo === null) {
464
            return $this->buildLocation($spiLocation);
465
        }
466
467
        return $this->mapLocation(
468
            $spiLocation,
469
            $this->buildContentInfoDomainObject($contentInfo),
470
            $this->buildContentProxy($contentInfo)
471
        );
472
    }
473
474
    public function buildLocation(
475
        SPILocation $spiLocation,
476
        array $prioritizedLanguages = [],
477
        bool $useAlwaysAvailable = true
478
    ): APILocation {
479
        if ($this->isRootLocation($spiLocation)) {
480
            return $this->buildRootLocation($spiLocation);
481
        }
482
483
        $spiContentInfo = $this->contentHandler->loadContentInfo($spiLocation->contentId);
484
485
        return $this->mapLocation(
486
            $spiLocation,
487
            $this->buildContentInfoDomainObject($spiContentInfo),
488
            $this->buildContentProxy($spiContentInfo, $prioritizedLanguages, $useAlwaysAvailable)
489
        );
490
    }
491
492
    /**
493
     * @param \eZ\Publish\SPI\Persistence\Content\Location $spiLocation
494
     * @param \eZ\Publish\API\Repository\Values\Content\Content|null $content
495
     * @param \eZ\Publish\SPI\Persistence\Content\ContentInfo|null $spiContentInfo
496
     *
497
     * @return \eZ\Publish\API\Repository\Values\Content\Location
498
     *
499
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
500
     */
501
    public function buildLocationWithContent(
502
        SPILocation $spiLocation,
503
        ?APIContent $content,
504
        ?SPIContentInfo $spiContentInfo = null
505
    ): APILocation {
506
        if ($this->isRootLocation($spiLocation)) {
507
            return $this->buildRootLocation($spiLocation);
508
        }
509
510
        if ($content === null) {
511
            throw new InvalidArgumentException('$content', "Location {$spiLocation->id} has missing Content");
512
        }
513
514
        if ($spiContentInfo !== null) {
515
            $contentInfo = $this->buildContentInfoDomainObject($spiContentInfo);
516
        } else {
517
            $contentInfo = $content->contentInfo;
518
        }
519
520
        return $this->mapLocation($spiLocation, $contentInfo, $content);
521
    }
522
523
    /**
524
     * Builds API Location object for tree root.
525
     *
526
     * @param \eZ\Publish\SPI\Persistence\Content\Location $spiLocation
527
     *
528
     * @return \eZ\Publish\API\Repository\Values\Content\Location
529
     */
530
    private function buildRootLocation(SPILocation $spiLocation): APILocation
531
    {
532
        //  first known commit of eZ Publish 3.x
533
        $legacyDateTime = $this->getDateTime(1030968000);
534
535
        // NOTE: this is hardcoded workaround for missing ContentInfo on root location
536
        return $this->mapLocation(
537
            $spiLocation,
538
            new ContentInfo([
539
                'id' => 0,
540
                'name' => 'Top Level Nodes',
541
                'sectionId' => 1,
542
                'mainLocationId' => 1,
543
                'contentTypeId' => 1,
544
                'currentVersionNo' => 1,
545
                'published' => 1,
546
                'ownerId' => 14, // admin user
547
                'modificationDate' => $legacyDateTime,
548
                'publishedDate' => $legacyDateTime,
549
                'alwaysAvailable' => 1,
550
                'remoteId' => null,
551
                'mainLanguageCode' => 'eng-GB',
552
            ]),
553
            new Content([])
554
        );
555
    }
556
557
    private function mapLocation(SPILocation $spiLocation, ContentInfo $contentInfo, APIContent $content): APILocation
558
    {
559
        return new Location(
560
            [
561
                'content' => $content,
562
                'contentInfo' => $contentInfo,
563
                'id' => $spiLocation->id,
564
                'priority' => $spiLocation->priority,
565
                'hidden' => $spiLocation->hidden || $contentInfo->isHidden,
566
                'invisible' => $spiLocation->invisible,
567
                'explicitlyHidden' => $spiLocation->hidden,
568
                'remoteId' => $spiLocation->remoteId,
569
                'parentLocationId' => $spiLocation->parentId,
570
                'pathString' => $spiLocation->pathString,
571
                'depth' => $spiLocation->depth,
572
                'sortField' => $spiLocation->sortField,
573
                'sortOrder' => $spiLocation->sortOrder,
574
            ]
575
        );
576
    }
577
578
    /**
579
     * Build API Content domain objects in bulk and apply to ContentSearchResult.
580
     *
581
     * Loading of Content objects are done in bulk.
582
     *
583
     * @param \eZ\Publish\API\Repository\Values\Content\Search\SearchResult $result SPI search result with SPI ContentInfo items as hits
584
     * @param array $languageFilter
585
     *
586
     * @return \eZ\Publish\SPI\Persistence\Content\ContentInfo[] ContentInfo we did not find content for is returned.
587
     */
588
    public function buildContentDomainObjectsOnSearchResult(SearchResult $result, array $languageFilter)
589
    {
590
        if (empty($result->searchHits)) {
591
            return [];
592
        }
593
594
        $contentIds = [];
595
        $contentTypeIds = [];
596
        $translations = $languageFilter['languages'] ?? [];
597
        $useAlwaysAvailable = $languageFilter['useAlwaysAvailable'] ?? true;
598
        foreach ($result->searchHits as $hit) {
599
            /** @var \eZ\Publish\SPI\Persistence\Content\ContentInfo $info */
600
            $info = $hit->valueObject;
601
            $contentIds[] = $info->id;
602
            $contentTypeIds[] = $info->contentTypeId;
603
            // Unless we are told to load all languages, we add main language to translations so they are loaded too
604
            // Might in some case load more languages then intended, but prioritised handling will pick right one
605
            if (!empty($languageFilter['languages']) && $useAlwaysAvailable && $info->alwaysAvailable) {
606
                $translations[] = $info->mainLanguageCode;
607
            }
608
        }
609
610
        $missingContentList = [];
611
        $contentList = $this->contentHandler->loadContentList($contentIds, array_unique($translations));
612
        $contentTypeList = $this->contentTypeHandler->loadContentTypeList(array_unique($contentTypeIds));
613
        foreach ($result->searchHits as $key => $hit) {
614
            if (isset($contentList[$hit->valueObject->id])) {
0 ignored issues
show
Documentation introduced by
The property id does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
615
                $hit->valueObject = $this->buildContentDomainObject(
616
                    $contentList[$hit->valueObject->id],
0 ignored issues
show
Documentation introduced by
The property id does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
617
                    $this->contentTypeDomainMapper->buildContentTypeDomainObject(
618
                        $contentTypeList[$hit->valueObject->contentTypeId],
0 ignored issues
show
Documentation introduced by
The property contentTypeId does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
619
                        $languageFilter['languages'] ?? []
620
                    ),
621
                    $languageFilter['languages'] ?? [],
622
                    $useAlwaysAvailable ? $hit->valueObject->mainLanguageCode : null
0 ignored issues
show
Documentation introduced by
The property mainLanguageCode does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
623
                );
624
            } else {
625
                $missingContentList[] = $hit->valueObject;
626
                unset($result->searchHits[$key]);
627
                --$result->totalCount;
628
            }
629
        }
630
631
        return $missingContentList;
632
    }
633
634
    /**
635
     * Build API Location and corresponding ContentInfo domain objects and apply to LocationSearchResult.
636
     *
637
     * This is done in order to be able to:
638
     * Load ContentInfo objects in bulk, generate proxy objects for Content that will loaded in bulk on-demand (on use).
639
     *
640
     * @param \eZ\Publish\API\Repository\Values\Content\Search\SearchResult $result SPI search result with SPI Location items as hits
641
     * @param array $languageFilter
642
     *
643
     * @return \eZ\Publish\SPI\Persistence\Content\Location[] Locations we did not find content info for is returned.
644
     */
645
    public function buildLocationDomainObjectsOnSearchResult(SearchResult $result, array $languageFilter)
646
    {
647
        if (empty($result->searchHits)) {
648
            return [];
649
        }
650
651
        $contentIds = [];
652
        foreach ($result->searchHits as $hit) {
653
            $contentIds[] = $hit->valueObject->contentId;
0 ignored issues
show
Documentation introduced by
The property contentId does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
654
        }
655
656
        $missingLocations = [];
657
        $contentInfoList = $this->contentHandler->loadContentInfoList($contentIds);
658
        $contentList = $this->buildContentProxyList(
659
            $contentInfoList,
660
            !empty($languageFilter['languages']) ? $languageFilter['languages'] : []
661
        );
662
        foreach ($result->searchHits as $key => $hit) {
663
            if (isset($contentInfoList[$hit->valueObject->contentId])) {
0 ignored issues
show
Documentation introduced by
The property contentId does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
664
                $hit->valueObject = $this->buildLocationWithContent(
665
                    $hit->valueObject,
0 ignored issues
show
Compatibility introduced by
$hit->valueObject of type object<eZ\Publish\API\Re...ory\Values\ValueObject> is not a sub-type of object<eZ\Publish\SPI\Pe...tence\Content\Location>. It seems like you assume a child class of the class eZ\Publish\API\Repository\Values\ValueObject to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
666
                    $contentList[$hit->valueObject->contentId],
0 ignored issues
show
Documentation introduced by
The property contentId does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
667
                    $contentInfoList[$hit->valueObject->contentId]
0 ignored issues
show
Documentation introduced by
The property contentId does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
668
                );
669
            } else {
670
                $missingLocations[] = $hit->valueObject;
671
                unset($result->searchHits[$key]);
672
                --$result->totalCount;
673
            }
674
        }
675
676
        return $missingLocations;
677
    }
678
679
    /**
680
     * Creates an array of SPI location create structs from given array of API location create structs.
681
     *
682
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
683
     *
684
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct $locationCreateStruct
685
     * @param \eZ\Publish\API\Repository\Values\Content\Location $parentLocation
686
     * @param mixed $mainLocation
687
     * @param mixed $contentId
688
     * @param mixed $contentVersionNo
689
     *
690
     * @return \eZ\Publish\SPI\Persistence\Content\Location\CreateStruct
691
     */
692
    public function buildSPILocationCreateStruct(
693
        $locationCreateStruct,
694
        APILocation $parentLocation,
695
        $mainLocation,
696
        $contentId,
697
        $contentVersionNo
698
    ) {
699
        if (!$this->isValidLocationPriority($locationCreateStruct->priority)) {
700
            throw new InvalidArgumentValue('priority', $locationCreateStruct->priority, 'LocationCreateStruct');
701
        }
702
703
        if (!is_bool($locationCreateStruct->hidden)) {
704
            throw new InvalidArgumentValue('hidden', $locationCreateStruct->hidden, 'LocationCreateStruct');
705
        }
706
707
        if ($locationCreateStruct->remoteId !== null && (!is_string($locationCreateStruct->remoteId) || empty($locationCreateStruct->remoteId))) {
708
            throw new InvalidArgumentValue('remoteId', $locationCreateStruct->remoteId, 'LocationCreateStruct');
709
        }
710
711
        if ($locationCreateStruct->sortField !== null && !$this->isValidLocationSortField($locationCreateStruct->sortField)) {
712
            throw new InvalidArgumentValue('sortField', $locationCreateStruct->sortField, 'LocationCreateStruct');
713
        }
714
715
        if ($locationCreateStruct->sortOrder !== null && !$this->isValidLocationSortOrder($locationCreateStruct->sortOrder)) {
716
            throw new InvalidArgumentValue('sortOrder', $locationCreateStruct->sortOrder, 'LocationCreateStruct');
717
        }
718
719
        $remoteId = $locationCreateStruct->remoteId;
720
        if (null === $remoteId) {
721
            $remoteId = $this->getUniqueHash($locationCreateStruct);
722
        } else {
723
            try {
724
                $this->locationHandler->loadByRemoteId($remoteId);
725
                throw new InvalidArgumentException(
726
                    '$locationCreateStructs',
727
                    "Another Location with remoteId '{$remoteId}' exists"
728
                );
729
            } catch (NotFoundException $e) {
730
                // Do nothing
731
            }
732
        }
733
734
        return new SPILocationCreateStruct(
735
            [
736
                'priority' => $locationCreateStruct->priority,
737
                'hidden' => $locationCreateStruct->hidden,
738
                // If we declare the new Location as hidden, it is automatically invisible
739
                // Otherwise it picks up visibility from parent Location
740
                // Note: There is no need to check for hidden status of parent, as hidden Location
741
                // is always invisible as well
742
                'invisible' => ($locationCreateStruct->hidden === true || $parentLocation->invisible),
743
                'remoteId' => $remoteId,
744
                'contentId' => $contentId,
745
                'contentVersion' => $contentVersionNo,
746
                // pathIdentificationString will be set in storage
747
                'pathIdentificationString' => null,
748
                'mainLocationId' => $mainLocation,
749
                'sortField' => $locationCreateStruct->sortField !== null ? $locationCreateStruct->sortField : Location::SORT_FIELD_NAME,
750
                'sortOrder' => $locationCreateStruct->sortOrder !== null ? $locationCreateStruct->sortOrder : Location::SORT_ORDER_ASC,
751
                'parentId' => $locationCreateStruct->parentLocationId,
752
            ]
753
        );
754
    }
755
756
    /**
757
     * Checks if given $sortField value is one of the defined sort field constants.
758
     *
759
     * @param mixed $sortField
760
     *
761
     * @return bool
762
     */
763
    public function isValidLocationSortField($sortField)
764
    {
765
        switch ($sortField) {
766
            case APILocation::SORT_FIELD_PATH:
767
            case APILocation::SORT_FIELD_PUBLISHED:
768
            case APILocation::SORT_FIELD_MODIFIED:
769
            case APILocation::SORT_FIELD_SECTION:
770
            case APILocation::SORT_FIELD_DEPTH:
771
            case APILocation::SORT_FIELD_CLASS_IDENTIFIER:
772
            case APILocation::SORT_FIELD_CLASS_NAME:
773
            case APILocation::SORT_FIELD_PRIORITY:
774
            case APILocation::SORT_FIELD_NAME:
775
            case APILocation::SORT_FIELD_MODIFIED_SUBNODE:
0 ignored issues
show
Deprecated Code introduced by
The constant eZ\Publish\API\Repositor..._FIELD_MODIFIED_SUBNODE has been deprecated.

This class constant has been deprecated.

Loading history...
776
            case APILocation::SORT_FIELD_NODE_ID:
777
            case APILocation::SORT_FIELD_CONTENTOBJECT_ID:
778
                return true;
779
        }
780
781
        return false;
782
    }
783
784
    /**
785
     * Checks if given $sortOrder value is one of the defined sort order constants.
786
     *
787
     * @param mixed $sortOrder
788
     *
789
     * @return bool
790
     */
791
    public function isValidLocationSortOrder($sortOrder)
792
    {
793
        switch ($sortOrder) {
794
            case APILocation::SORT_ORDER_DESC:
795
            case APILocation::SORT_ORDER_ASC:
796
                return true;
797
        }
798
799
        return false;
800
    }
801
802
    /**
803
     * Checks if given $priority is valid.
804
     *
805
     * @param int $priority
806
     *
807
     * @return bool
808
     */
809
    public function isValidLocationPriority($priority)
810
    {
811
        if ($priority === null) {
812
            return true;
813
        }
814
815
        return is_int($priority) && $priority >= self::MIN_LOCATION_PRIORITY && $priority <= self::MAX_LOCATION_PRIORITY;
816
    }
817
818
    /**
819
     * Validates given translated list $list, which should be an array of strings with language codes as keys.
820
     *
821
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
822
     *
823
     * @param mixed $list
824
     * @param string $argumentName
825
     */
826
    public function validateTranslatedList($list, $argumentName)
827
    {
828
        if (!is_array($list)) {
829
            throw new InvalidArgumentType($argumentName, 'array', $list);
830
        }
831
832
        foreach ($list as $languageCode => $translation) {
833
            $this->contentLanguageHandler->loadByLanguageCode($languageCode);
834
835
            if (!is_string($translation)) {
836
                throw new InvalidArgumentType($argumentName . "['$languageCode']", 'string', $translation);
837
            }
838
        }
839
    }
840
841
    /**
842
     * Returns \DateTime object from given $timestamp in environment timezone.
843
     *
844
     * This method is needed because constructing \DateTime with $timestamp will
845
     * return the object in UTC timezone.
846
     *
847
     * @param int $timestamp
848
     *
849
     * @return \DateTime
850
     */
851
    public function getDateTime($timestamp)
852
    {
853
        $dateTime = new DateTime();
854
        $dateTime->setTimestamp($timestamp);
855
856
        return $dateTime;
857
    }
858
859
    /**
860
     * Creates unique hash string for given $object.
861
     *
862
     * Used for remoteId.
863
     *
864
     * @param object $object
865
     *
866
     * @return string
867
     */
868
    public function getUniqueHash($object)
869
    {
870
        return md5(uniqid(get_class($object), true));
871
    }
872
873
    /**
874
     * Returns true if given location is a tree root.
875
     *
876
     * @param \eZ\Publish\SPI\Persistence\Content\Location $spiLocation
877
     *
878
     * @return bool
879
     */
880
    private function isRootLocation(SPILocation $spiLocation): bool
881
    {
882
        return $spiLocation->id === $spiLocation->parentId;
883
    }
884
}
885