Completed
Push — qa-cumulative-ezp-31278-31279-... ( 4a8f9c )
by
unknown
14:58
created

ContentDomainMapper::buildContentProxyList()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 3
dl 0
loc 13
rs 9.8333
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
5
 * @license For full copyright and license information view LICENSE file distributed with this source code.
6
 */
7
namespace eZ\Publish\Core\Repository\Mapper;
8
9
use eZ\Publish\API\Repository\Values\Content\Search\SearchResult;
10
use eZ\Publish\API\Repository\Values\Content\Content as APIContent;
11
use eZ\Publish\Core\FieldType\FieldTypeRegistry;
12
use eZ\Publish\SPI\Persistence\Content\Handler as ContentHandler;
13
use eZ\Publish\SPI\Persistence\Content\Location\Handler as LocationHandler;
14
use eZ\Publish\SPI\Persistence\Content\Language\Handler as LanguageHandler;
15
use eZ\Publish\SPI\Persistence\Content\Type\Handler as TypeHandler;
16
use eZ\Publish\Core\Repository\Values\Content\Content;
17
use eZ\Publish\Core\Repository\Values\Content\ContentProxy;
18
use eZ\Publish\API\Repository\Values\Content\VersionInfo as APIVersionInfo;
19
use eZ\Publish\Core\Repository\Values\Content\VersionInfo;
20
use eZ\Publish\API\Repository\Values\Content\ContentInfo;
21
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
22
use eZ\Publish\API\Repository\Values\Content\Field;
23
use eZ\Publish\Core\Repository\Values\Content\Relation;
24
use eZ\Publish\API\Repository\Values\Content\Location as APILocation;
25
use eZ\Publish\Core\Repository\Values\Content\Location;
26
use eZ\Publish\SPI\Persistence\Content as SPIContent;
27
use eZ\Publish\SPI\Persistence\Content\Location as SPILocation;
28
use eZ\Publish\SPI\Persistence\Content\VersionInfo as SPIVersionInfo;
29
use eZ\Publish\SPI\Persistence\Content\ContentInfo as SPIContentInfo;
30
use eZ\Publish\SPI\Persistence\Content\Relation as SPIRelation;
31
use eZ\Publish\SPI\Persistence\Content\Type as SPIContentType;
32
use eZ\Publish\SPI\Persistence\Content\Location\CreateStruct as SPILocationCreateStruct;
33
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
34
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
35
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue;
36
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentType;
37
use DateTime;
38
use eZ\Publish\SPI\Repository\Strategy\ContentThumbnail\ThumbnailStrategy;
39
40
/**
41
 * ContentDomainMapper is an internal service.
42
 *
43
 * @internal Meant for internal use by Repository.
44
 */
45
class ContentDomainMapper
46
{
47
    const MAX_LOCATION_PRIORITY = 2147483647;
48
    const MIN_LOCATION_PRIORITY = -2147483648;
49
50
    /** @var \eZ\Publish\SPI\Persistence\Content\Handler */
51
    protected $contentHandler;
52
53
    /** @var \eZ\Publish\SPI\Persistence\Content\Location\Handler */
54
    protected $locationHandler;
55
56
    /** @var \eZ\Publish\SPI\Persistence\Content\Type\Handler */
57
    protected $contentTypeHandler;
58
59
    /** @var \eZ\Publish\Core\Repository\Mapper\ContentTypeDomainMapper */
60
    protected $contentTypeDomainMapper;
61
62
    /** @var \eZ\Publish\SPI\Persistence\Content\Language\Handler */
63
    protected $contentLanguageHandler;
64
65
    /** @var \eZ\Publish\Core\Repository\Helper\FieldTypeRegistry */
66
    protected $fieldTypeRegistry;
67
68
    /** @var \eZ\Publish\SPI\Repository\Strategy\ContentThumbnail\ThumbnailStrategy */
69
    private $thumbnailStrategy;
70
71
    /**
72
     * Setups service with reference to repository.
73
     *
74
     * @param \eZ\Publish\SPI\Persistence\Content\Handler $contentHandler
75
     * @param \eZ\Publish\SPI\Persistence\Content\Location\Handler $locationHandler
76
     * @param \eZ\Publish\SPI\Persistence\Content\Type\Handler $contentTypeHandler
77
     * @param \eZ\Publish\Core\Repository\Mapper\ContentTypeDomainMapper $contentTypeDomainMapper
78
     * @param \eZ\Publish\SPI\Persistence\Content\Language\Handler $contentLanguageHandler
79
     * @param \eZ\Publish\Core\FieldType\FieldTypeRegistry $fieldTypeRegistry
80
     * @param \eZ\Publish\SPI\Repository\Strategy\ContentThumbnail\ThumbnailStrategy $thumbnailStrategy
81
     */
82
    public function __construct(
83
        ContentHandler $contentHandler,
84
        LocationHandler $locationHandler,
85
        TypeHandler $contentTypeHandler,
86
        ContentTypeDomainMapper $contentTypeDomainMapper,
87
        LanguageHandler $contentLanguageHandler,
88
        FieldTypeRegistry $fieldTypeRegistry,
89
        ThumbnailStrategy $thumbnailStrategy
90
    ) {
91
        $this->contentHandler = $contentHandler;
92
        $this->locationHandler = $locationHandler;
93
        $this->contentTypeHandler = $contentTypeHandler;
94
        $this->contentTypeDomainMapper = $contentTypeDomainMapper;
95
        $this->contentLanguageHandler = $contentLanguageHandler;
96
        $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...
97
        $this->thumbnailStrategy = $thumbnailStrategy;
98
    }
99
100
    /**
101
     * Builds a Content domain object from value object.
102
     *
103
     * @param \eZ\Publish\SPI\Persistence\Content $spiContent
104
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
105
     * @param array $prioritizedLanguages Prioritized language codes to filter fields on
106
     * @param string|null $fieldAlwaysAvailableLanguage Language code fallback if a given field is not found in $prioritizedLanguages
107
     *
108
     * @return \eZ\Publish\Core\Repository\Values\Content\Content
109
     */
110
    public function buildContentDomainObject(
111
        SPIContent $spiContent,
112
        ContentType $contentType,
113
        array $prioritizedLanguages = [],
114
        string $fieldAlwaysAvailableLanguage = null
115
    ) {
116
        $prioritizedFieldLanguageCode = null;
117
        if (!empty($prioritizedLanguages)) {
118
            $availableFieldLanguageMap = array_fill_keys($spiContent->versionInfo->languageCodes, true);
119
            foreach ($prioritizedLanguages as $prioritizedLanguage) {
120
                if (isset($availableFieldLanguageMap[$prioritizedLanguage])) {
121
                    $prioritizedFieldLanguageCode = $prioritizedLanguage;
122
                    break;
123
                }
124
            }
125
        }
126
127
        $internalFields = $this->buildDomainFields($spiContent->fields, $contentType, $prioritizedLanguages, $fieldAlwaysAvailableLanguage);
128
129
        return new Content(
130
            [
131
                'thumbnail' => $this->thumbnailStrategy->getThumbnail($contentType, $internalFields),
132
                'internalFields' => $internalFields,
133
                'versionInfo' => $this->buildVersionInfoDomainObject($spiContent->versionInfo, $prioritizedLanguages),
134
                'contentType' => $contentType,
135
                'prioritizedFieldLanguageCode' => $prioritizedFieldLanguageCode,
136
            ]
137
        );
138
    }
139
140
    /**
141
     * Builds a Content domain object from value object returned from persistence.
142
     *
143
     * @param \eZ\Publish\SPI\Persistence\Content $spiContent
144
     * @param \eZ\Publish\SPI\Persistence\Content\Type $spiContentType
145
     * @param string[] $prioritizedLanguages Prioritized language codes to filter fields on
146
     * @param string|null $fieldAlwaysAvailableLanguage Language code fallback if a given field is not found in $prioritizedLanguages
147
     *
148
     * @return \eZ\Publish\Core\Repository\Values\Content\Content
149
     */
150
    public function buildContentDomainObjectFromPersistence(
151
        SPIContent $spiContent,
152
        SPIContentType $spiContentType,
153
        array $prioritizedLanguages = [],
154
        ?string $fieldAlwaysAvailableLanguage = null
155
    ): APIContent {
156
        $contentType = $this->contentTypeDomainMapper->buildContentTypeDomainObject($spiContentType, $prioritizedLanguages);
157
158
        return $this->buildContentDomainObject($spiContent, $contentType, $prioritizedLanguages, $fieldAlwaysAvailableLanguage);
159
    }
160
161
    /**
162
     * Builds a Content proxy object (lazy loaded, loads as soon as used).
163
     */
164
    public function buildContentProxy(
165
        SPIContent\ContentInfo $info,
166
        array $prioritizedLanguages = [],
167
        bool $useAlwaysAvailable = true
168
    ): APIContent {
169
        $generator = $this->generatorForContentList([$info], $prioritizedLanguages, $useAlwaysAvailable);
170
171
        return new ContentProxy($generator, $info->id);
172
    }
173
174
    /**
175
     * Builds a list of Content proxy objects (lazy loaded, loads all as soon as one of them loads).
176
     *
177
     * @param \eZ\Publish\SPI\Persistence\Content\ContentInfo[] $infoList
178
     * @param string[] $prioritizedLanguages
179
     * @param bool $useAlwaysAvailable
180
     *
181
     * @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...
182
     */
183
    public function buildContentProxyList(
184
        array $infoList,
185
        array $prioritizedLanguages = [],
186
        bool $useAlwaysAvailable = true
187
    ): array {
188
        $list = [];
189
        $generator = $this->generatorForContentList($infoList, $prioritizedLanguages, $useAlwaysAvailable);
190
        foreach ($infoList as $info) {
191
            $list[$info->id] = new ContentProxy($generator, $info->id);
192
        }
193
194
        return $list;
195
    }
196
197
    /**
198
     * @todo Maybe change signature to generatorForContentList($contentIds, $prioritizedLanguages, $translations)
199
     * @todo to avoid keeping referance to $infoList all the way until the generator is called.
200
     *
201
     * @param \eZ\Publish\SPI\Persistence\Content\ContentInfo[] $infoList
202
     * @param string[] $prioritizedLanguages
203
     * @param bool $useAlwaysAvailable
204
     *
205
     * @return \Generator
206
     */
207
    private function generatorForContentList(
208
        array $infoList,
209
        array $prioritizedLanguages = [],
210
        bool $useAlwaysAvailable = true
211
    ): \Generator {
212
        $contentIds = [];
213
        $contentTypeIds = [];
214
        $translations = $prioritizedLanguages;
215
        foreach ($infoList as $info) {
216
            $contentIds[] = $info->id;
217
            $contentTypeIds[] = $info->contentTypeId;
218
            // Unless we are told to load all languages, we add main language to translations so they are loaded too
219
            // Might in some case load more languages then intended, but prioritised handling will pick right one
220
            if (!empty($prioritizedLanguages) && $useAlwaysAvailable && $info->alwaysAvailable) {
221
                $translations[] = $info->mainLanguageCode;
222
            }
223
        }
224
225
        unset($infoList);
226
227
        $contentList = $this->contentHandler->loadContentList($contentIds, array_unique($translations));
228
        $contentTypeList = $this->contentTypeHandler->loadContentTypeList(array_unique($contentTypeIds));
229
        while (!empty($contentList)) {
230
            $id = yield;
231
            /** @var \eZ\Publish\SPI\Persistence\Content\ContentInfo $info */
232
            $info = $contentList[$id]->versionInfo->contentInfo;
233
            yield $this->buildContentDomainObject(
234
                $contentList[$id],
235
                $this->contentTypeDomainMapper->buildContentTypeDomainObject(
236
                    $contentTypeList[$info->contentTypeId],
237
                    $prioritizedLanguages
238
                ),
239
                $prioritizedLanguages,
240
                $info->alwaysAvailable ? $info->mainLanguageCode : null
241
            );
242
243
            unset($contentList[$id]);
244
        }
245
    }
246
247
    /**
248
     * Returns an array of domain fields created from given array of SPI fields.
249
     *
250
     * @throws InvalidArgumentType On invalid $contentType
251
     *
252
     * @param \eZ\Publish\SPI\Persistence\Content\Field[] $spiFields
253
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType|\eZ\Publish\SPI\Persistence\Content\Type $contentType
254
     * @param array $prioritizedLanguages A language priority, filters returned fields and is used as prioritized language code on
255
     *                         returned value object. If not given all languages are returned.
256
     * @param string|null $alwaysAvailableLanguage Language code fallback if a given field is not found in $prioritizedLanguages
257
     *
258
     * @return array
259
     */
260
    public function buildDomainFields(
261
        array $spiFields,
262
        $contentType,
263
        array $prioritizedLanguages = [],
264
        string $alwaysAvailableLanguage = null
265
    ) {
266
        if (!$contentType instanceof SPIContentType && !$contentType instanceof ContentType) {
267
            throw new InvalidArgumentType('$contentType', 'SPI ContentType | API ContentType');
268
        }
269
270
        $fieldDefinitionsMap = [];
271
        foreach ($contentType->fieldDefinitions as $fieldDefinition) {
272
            $fieldDefinitionsMap[$fieldDefinition->id] = $fieldDefinition;
273
        }
274
275
        $fieldInFilterLanguagesMap = [];
276
        if (!empty($prioritizedLanguages) && $alwaysAvailableLanguage !== null) {
277
            foreach ($spiFields as $spiField) {
278
                if (in_array($spiField->languageCode, $prioritizedLanguages)) {
279
                    $fieldInFilterLanguagesMap[$spiField->fieldDefinitionId] = true;
280
                }
281
            }
282
        }
283
284
        $fields = [];
285
        foreach ($spiFields as $spiField) {
286
            // We ignore fields in content not part of the content type
287
            if (!isset($fieldDefinitionsMap[$spiField->fieldDefinitionId])) {
288
                continue;
289
            }
290
291
            $fieldDefinition = $fieldDefinitionsMap[$spiField->fieldDefinitionId];
292
293
            if (!empty($prioritizedLanguages) && !in_array($spiField->languageCode, $prioritizedLanguages)) {
294
                // If filtering is enabled we ignore fields in other languages then $prioritizedLanguages, if:
295
                if ($alwaysAvailableLanguage === null) {
296
                    // Ignore field if we don't have $alwaysAvailableLanguageCode fallback
297
                    continue;
298
                } elseif (!empty($fieldInFilterLanguagesMap[$spiField->fieldDefinitionId])) {
299
                    // Ignore field if it exists in one of the filtered languages
300
                    continue;
301
                } elseif ($spiField->languageCode !== $alwaysAvailableLanguage) {
302
                    // Also ignore if field is not in $alwaysAvailableLanguageCode
303
                    continue;
304
                }
305
            }
306
307
            $fields[$fieldDefinition->position][] = new Field(
308
                [
309
                    'id' => $spiField->id,
310
                    'value' => $this->fieldTypeRegistry->getFieldType($spiField->type)
311
                        ->fromPersistenceValue($spiField->value),
312
                    'languageCode' => $spiField->languageCode,
313
                    'fieldDefIdentifier' => $fieldDefinition->identifier,
314
                    'fieldTypeIdentifier' => $spiField->type,
315
                ]
316
            );
317
        }
318
319
        // Sort fields by content type field definition priority
320
        ksort($fields, SORT_NUMERIC);
321
322
        // Flatten array
323
        return array_merge(...$fields);
324
    }
325
326
    /**
327
     * Builds a VersionInfo domain object from value object returned from persistence.
328
     *
329
     * @param \eZ\Publish\SPI\Persistence\Content\VersionInfo $spiVersionInfo
330
     * @param array $prioritizedLanguages
331
     *
332
     * @return \eZ\Publish\Core\Repository\Values\Content\VersionInfo
333
     */
334
    public function buildVersionInfoDomainObject(SPIVersionInfo $spiVersionInfo, array $prioritizedLanguages = [])
335
    {
336
        // Map SPI statuses to API
337
        switch ($spiVersionInfo->status) {
338
            case SPIVersionInfo::STATUS_ARCHIVED:
339
                $status = APIVersionInfo::STATUS_ARCHIVED;
340
                break;
341
342
            case SPIVersionInfo::STATUS_PUBLISHED:
343
                $status = APIVersionInfo::STATUS_PUBLISHED;
344
                break;
345
346
            case SPIVersionInfo::STATUS_DRAFT:
347
            default:
348
                $status = APIVersionInfo::STATUS_DRAFT;
349
        }
350
351
        // Find prioritised language among names
352
        $prioritizedNameLanguageCode = null;
353
        foreach ($prioritizedLanguages as $prioritizedLanguage) {
354
            if (isset($spiVersionInfo->names[$prioritizedLanguage])) {
355
                $prioritizedNameLanguageCode = $prioritizedLanguage;
356
                break;
357
            }
358
        }
359
360
        return new VersionInfo(
361
            [
362
                'id' => $spiVersionInfo->id,
363
                'versionNo' => $spiVersionInfo->versionNo,
364
                'modificationDate' => $this->getDateTime($spiVersionInfo->modificationDate),
365
                'creatorId' => $spiVersionInfo->creatorId,
366
                'creationDate' => $this->getDateTime($spiVersionInfo->creationDate),
367
                'status' => $status,
368
                'initialLanguageCode' => $spiVersionInfo->initialLanguageCode,
369
                'languageCodes' => $spiVersionInfo->languageCodes,
370
                'names' => $spiVersionInfo->names,
371
                'contentInfo' => $this->buildContentInfoDomainObject($spiVersionInfo->contentInfo),
372
                'prioritizedNameLanguageCode' => $prioritizedNameLanguageCode,
373
            ]
374
        );
375
    }
376
377
    /**
378
     * Builds a ContentInfo domain object from value object returned from persistence.
379
     *
380
     * @param \eZ\Publish\SPI\Persistence\Content\ContentInfo $spiContentInfo
381
     *
382
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
383
     */
384
    public function buildContentInfoDomainObject(SPIContentInfo $spiContentInfo)
385
    {
386
        // Map SPI statuses to API
387
        switch ($spiContentInfo->status) {
388
            case SPIContentInfo::STATUS_TRASHED:
389
                $status = ContentInfo::STATUS_TRASHED;
390
                break;
391
392
            case SPIContentInfo::STATUS_PUBLISHED:
393
                $status = ContentInfo::STATUS_PUBLISHED;
394
                break;
395
396
            case SPIContentInfo::STATUS_DRAFT:
397
            default:
398
                $status = ContentInfo::STATUS_DRAFT;
399
        }
400
401
        return new ContentInfo(
402
            [
403
                'id' => $spiContentInfo->id,
404
                'contentTypeId' => $spiContentInfo->contentTypeId,
405
                'name' => $spiContentInfo->name,
406
                'sectionId' => $spiContentInfo->sectionId,
407
                'currentVersionNo' => $spiContentInfo->currentVersionNo,
408
                'published' => $spiContentInfo->isPublished,
409
                'ownerId' => $spiContentInfo->ownerId,
410
                'modificationDate' => $spiContentInfo->modificationDate == 0 ?
411
                    null :
412
                    $this->getDateTime($spiContentInfo->modificationDate),
413
                'publishedDate' => $spiContentInfo->publicationDate == 0 ?
414
                    null :
415
                    $this->getDateTime($spiContentInfo->publicationDate),
416
                'alwaysAvailable' => $spiContentInfo->alwaysAvailable,
417
                'remoteId' => $spiContentInfo->remoteId,
418
                'mainLanguageCode' => $spiContentInfo->mainLanguageCode,
419
                'mainLocationId' => $spiContentInfo->mainLocationId,
420
                'status' => $status,
421
                'isHidden' => $spiContentInfo->isHidden,
422
            ]
423
        );
424
    }
425
426
    /**
427
     * Builds API Relation object from provided SPI Relation object.
428
     *
429
     * @param \eZ\Publish\SPI\Persistence\Content\Relation $spiRelation
430
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $sourceContentInfo
431
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $destinationContentInfo
432
     *
433
     * @return \eZ\Publish\API\Repository\Values\Content\Relation
434
     */
435
    public function buildRelationDomainObject(
436
        SPIRelation $spiRelation,
437
        ContentInfo $sourceContentInfo,
438
        ContentInfo $destinationContentInfo
439
    ) {
440
        $sourceFieldDefinitionIdentifier = null;
441
        if ($spiRelation->sourceFieldDefinitionId !== null) {
442
            $contentType = $this->contentTypeHandler->load($sourceContentInfo->contentTypeId);
443
            foreach ($contentType->fieldDefinitions as $fieldDefinition) {
444
                if ($fieldDefinition->id !== $spiRelation->sourceFieldDefinitionId) {
445
                    continue;
446
                }
447
448
                $sourceFieldDefinitionIdentifier = $fieldDefinition->identifier;
449
                break;
450
            }
451
        }
452
453
        return new Relation(
454
            [
455
                'id' => $spiRelation->id,
456
                'sourceFieldDefinitionIdentifier' => $sourceFieldDefinitionIdentifier,
457
                'type' => $spiRelation->type,
458
                'sourceContentInfo' => $sourceContentInfo,
459
                'destinationContentInfo' => $destinationContentInfo,
460
            ]
461
        );
462
    }
463
464
    /**
465
     * @deprecated Since 7.2, use buildLocationWithContent(), buildLocation() or (private) mapLocation() instead.
466
     */
467
    public function buildLocationDomainObject(
468
        SPILocation $spiLocation,
469
        SPIContentInfo $contentInfo = null
470
    ) {
471
        if ($contentInfo === null) {
472
            return $this->buildLocation($spiLocation);
473
        }
474
475
        return $this->mapLocation(
476
            $spiLocation,
477
            $this->buildContentInfoDomainObject($contentInfo),
478
            $this->buildContentProxy($contentInfo)
479
        );
480
    }
481
482
    public function buildLocation(
483
        SPILocation $spiLocation,
484
        array $prioritizedLanguages = [],
485
        bool $useAlwaysAvailable = true
486
    ): APILocation {
487
        if ($this->isRootLocation($spiLocation)) {
488
            return $this->buildRootLocation($spiLocation);
489
        }
490
491
        $spiContentInfo = $this->contentHandler->loadContentInfo($spiLocation->contentId);
492
493
        return $this->mapLocation(
494
            $spiLocation,
495
            $this->buildContentInfoDomainObject($spiContentInfo),
496
            $this->buildContentProxy($spiContentInfo, $prioritizedLanguages, $useAlwaysAvailable)
497
        );
498
    }
499
500
    /**
501
     * @param \eZ\Publish\SPI\Persistence\Content\Location $spiLocation
502
     * @param \eZ\Publish\API\Repository\Values\Content\Content|null $content
503
     * @param \eZ\Publish\SPI\Persistence\Content\ContentInfo|null $spiContentInfo
504
     *
505
     * @return \eZ\Publish\API\Repository\Values\Content\Location
506
     *
507
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
508
     */
509
    public function buildLocationWithContent(
510
        SPILocation $spiLocation,
511
        ?APIContent $content,
512
        ?SPIContentInfo $spiContentInfo = null
513
    ): APILocation {
514
        if ($this->isRootLocation($spiLocation)) {
515
            return $this->buildRootLocation($spiLocation);
516
        }
517
518
        if ($content === null) {
519
            throw new InvalidArgumentException('$content', "Location {$spiLocation->id} has missing Content");
520
        }
521
522
        if ($spiContentInfo !== null) {
523
            $contentInfo = $this->buildContentInfoDomainObject($spiContentInfo);
524
        } else {
525
            $contentInfo = $content->contentInfo;
526
        }
527
528
        return $this->mapLocation($spiLocation, $contentInfo, $content);
529
    }
530
531
    /**
532
     * Builds API Location object for tree root.
533
     *
534
     * @param \eZ\Publish\SPI\Persistence\Content\Location $spiLocation
535
     *
536
     * @return \eZ\Publish\API\Repository\Values\Content\Location
537
     */
538
    private function buildRootLocation(SPILocation $spiLocation): APILocation
539
    {
540
        //  first known commit of eZ Publish 3.x
541
        $legacyDateTime = $this->getDateTime(1030968000);
542
543
        $contentInfo = new ContentInfo([
544
            'id' => 0,
545
            'name' => 'Top Level Nodes',
546
            'sectionId' => 1,
547
            'mainLocationId' => 1,
548
            'contentTypeId' => 1,
549
            'currentVersionNo' => 1,
550
            'published' => 1,
551
            'ownerId' => 14, // admin user
552
            'modificationDate' => $legacyDateTime,
553
            'publishedDate' => $legacyDateTime,
554
            'alwaysAvailable' => 1,
555
            'remoteId' => null,
556
            'mainLanguageCode' => 'eng-GB',
557
        ]);
558
559
        $content = new Content([
560
            'versionInfo' => new VersionInfo([
561
                'names' => [
562
                    $contentInfo->mainLanguageCode => $contentInfo->name,
563
                ],
564
                'contentInfo' => $contentInfo,
565
                'versionNo' => $contentInfo->currentVersionNo,
566
                'modificationDate' => $contentInfo->modificationDate,
567
                'creationDate' => $contentInfo->modificationDate,
568
                'creatorId' => $contentInfo->ownerId,
569
            ]),
570
        ]);
571
572
        // NOTE: this is hardcoded workaround for missing ContentInfo on root location
573
        return $this->mapLocation(
574
            $spiLocation,
575
            $contentInfo,
576
            $content
577
        );
578
    }
579
580
    private function mapLocation(SPILocation $spiLocation, ContentInfo $contentInfo, APIContent $content): APILocation
581
    {
582
        return new Location(
583
            [
584
                'content' => $content,
585
                'contentInfo' => $contentInfo,
586
                'id' => $spiLocation->id,
587
                'priority' => $spiLocation->priority,
588
                'hidden' => $spiLocation->hidden || $contentInfo->isHidden,
589
                'invisible' => $spiLocation->invisible,
590
                'explicitlyHidden' => $spiLocation->hidden,
591
                'remoteId' => $spiLocation->remoteId,
592
                'parentLocationId' => $spiLocation->parentId,
593
                'pathString' => $spiLocation->pathString,
594
                'depth' => $spiLocation->depth,
595
                'sortField' => $spiLocation->sortField,
596
                'sortOrder' => $spiLocation->sortOrder,
597
            ]
598
        );
599
    }
600
601
    /**
602
     * Build API Content domain objects in bulk and apply to ContentSearchResult.
603
     *
604
     * Loading of Content objects are done in bulk.
605
     *
606
     * @param \eZ\Publish\API\Repository\Values\Content\Search\SearchResult $result SPI search result with SPI ContentInfo items as hits
607
     * @param array $languageFilter
608
     *
609
     * @return \eZ\Publish\SPI\Persistence\Content\ContentInfo[] ContentInfo we did not find content for is returned.
610
     */
611
    public function buildContentDomainObjectsOnSearchResult(SearchResult $result, array $languageFilter)
612
    {
613
        if (empty($result->searchHits)) {
614
            return [];
615
        }
616
617
        $contentIds = [];
618
        $contentTypeIds = [];
619
        $translations = $languageFilter['languages'] ?? [];
620
        $useAlwaysAvailable = $languageFilter['useAlwaysAvailable'] ?? true;
621
        foreach ($result->searchHits as $hit) {
622
            /** @var \eZ\Publish\SPI\Persistence\Content\ContentInfo $info */
623
            $info = $hit->valueObject;
624
            $contentIds[] = $info->id;
625
            $contentTypeIds[] = $info->contentTypeId;
626
            // Unless we are told to load all languages, we add main language to translations so they are loaded too
627
            // Might in some case load more languages then intended, but prioritised handling will pick right one
628
            if (!empty($languageFilter['languages']) && $useAlwaysAvailable && $info->alwaysAvailable) {
629
                $translations[] = $info->mainLanguageCode;
630
            }
631
        }
632
633
        $missingContentList = [];
634
        $contentList = $this->contentHandler->loadContentList($contentIds, array_unique($translations));
635
        $contentTypeList = $this->contentTypeHandler->loadContentTypeList(array_unique($contentTypeIds));
636
        foreach ($result->searchHits as $key => $hit) {
637
            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...
638
                $hit->valueObject = $this->buildContentDomainObject(
639
                    $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...
640
                    $this->contentTypeDomainMapper->buildContentTypeDomainObject(
641
                        $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...
642
                        $languageFilter['languages'] ?? []
643
                    ),
644
                    $languageFilter['languages'] ?? [],
645
                    $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...
646
                );
647
            } else {
648
                $missingContentList[] = $hit->valueObject;
649
                unset($result->searchHits[$key]);
650
                --$result->totalCount;
651
            }
652
        }
653
654
        return $missingContentList;
655
    }
656
657
    /**
658
     * Build API Location and corresponding ContentInfo domain objects and apply to LocationSearchResult.
659
     *
660
     * This is done in order to be able to:
661
     * Load ContentInfo objects in bulk, generate proxy objects for Content that will loaded in bulk on-demand (on use).
662
     *
663
     * @param \eZ\Publish\API\Repository\Values\Content\Search\SearchResult $result SPI search result with SPI Location items as hits
664
     * @param array $languageFilter
665
     *
666
     * @return \eZ\Publish\SPI\Persistence\Content\Location[] Locations we did not find content info for is returned.
667
     */
668
    public function buildLocationDomainObjectsOnSearchResult(SearchResult $result, array $languageFilter)
669
    {
670
        if (empty($result->searchHits)) {
671
            return [];
672
        }
673
674
        $contentIds = [];
675
        foreach ($result->searchHits as $hit) {
676
            $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...
677
        }
678
679
        $missingLocations = [];
680
        $contentInfoList = $this->contentHandler->loadContentInfoList($contentIds);
681
        $contentList = $this->buildContentProxyList(
682
            $contentInfoList,
683
            !empty($languageFilter['languages']) ? $languageFilter['languages'] : []
684
        );
685
        foreach ($result->searchHits as $key => $hit) {
686
            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...
687
                $hit->valueObject = $this->buildLocationWithContent(
688
                    $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...
689
                    $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...
690
                    $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...
691
                );
692
            } else {
693
                $missingLocations[] = $hit->valueObject;
694
                unset($result->searchHits[$key]);
695
                --$result->totalCount;
696
            }
697
        }
698
699
        return $missingLocations;
700
    }
701
702
    /**
703
     * Creates an array of SPI location create structs from given array of API location create structs.
704
     *
705
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
706
     *
707
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct $locationCreateStruct
708
     * @param \eZ\Publish\API\Repository\Values\Content\Location $parentLocation
709
     * @param mixed $mainLocation
710
     * @param mixed $contentId
711
     * @param mixed $contentVersionNo
712
     *
713
     * @return \eZ\Publish\SPI\Persistence\Content\Location\CreateStruct
714
     */
715
    public function buildSPILocationCreateStruct(
716
        $locationCreateStruct,
717
        APILocation $parentLocation,
718
        $mainLocation,
719
        $contentId,
720
        $contentVersionNo
721
    ) {
722
        if (!$this->isValidLocationPriority($locationCreateStruct->priority)) {
723
            throw new InvalidArgumentValue('priority', $locationCreateStruct->priority, 'LocationCreateStruct');
724
        }
725
726
        if (!is_bool($locationCreateStruct->hidden)) {
727
            throw new InvalidArgumentValue('hidden', $locationCreateStruct->hidden, 'LocationCreateStruct');
728
        }
729
730
        if ($locationCreateStruct->remoteId !== null && (!is_string($locationCreateStruct->remoteId) || empty($locationCreateStruct->remoteId))) {
731
            throw new InvalidArgumentValue('remoteId', $locationCreateStruct->remoteId, 'LocationCreateStruct');
732
        }
733
734
        if ($locationCreateStruct->sortField !== null && !$this->isValidLocationSortField($locationCreateStruct->sortField)) {
735
            throw new InvalidArgumentValue('sortField', $locationCreateStruct->sortField, 'LocationCreateStruct');
736
        }
737
738
        if ($locationCreateStruct->sortOrder !== null && !$this->isValidLocationSortOrder($locationCreateStruct->sortOrder)) {
739
            throw new InvalidArgumentValue('sortOrder', $locationCreateStruct->sortOrder, 'LocationCreateStruct');
740
        }
741
742
        $remoteId = $locationCreateStruct->remoteId;
743
        if (null === $remoteId) {
744
            $remoteId = $this->getUniqueHash($locationCreateStruct);
745
        } else {
746
            try {
747
                $this->locationHandler->loadByRemoteId($remoteId);
748
                throw new InvalidArgumentException(
749
                    '$locationCreateStructs',
750
                    "Another Location with remoteId '{$remoteId}' exists"
751
                );
752
            } catch (NotFoundException $e) {
753
                // Do nothing
754
            }
755
        }
756
757
        return new SPILocationCreateStruct(
758
            [
759
                'priority' => $locationCreateStruct->priority,
760
                'hidden' => $locationCreateStruct->hidden,
761
                // If we declare the new Location as hidden, it is automatically invisible
762
                // Otherwise it picks up visibility from parent Location
763
                // Note: There is no need to check for hidden status of parent, as hidden Location
764
                // is always invisible as well
765
                'invisible' => ($locationCreateStruct->hidden === true || $parentLocation->invisible),
766
                'remoteId' => $remoteId,
767
                'contentId' => $contentId,
768
                'contentVersion' => $contentVersionNo,
769
                // pathIdentificationString will be set in storage
770
                'pathIdentificationString' => null,
771
                'mainLocationId' => $mainLocation,
772
                'sortField' => $locationCreateStruct->sortField !== null ? $locationCreateStruct->sortField : Location::SORT_FIELD_NAME,
773
                'sortOrder' => $locationCreateStruct->sortOrder !== null ? $locationCreateStruct->sortOrder : Location::SORT_ORDER_ASC,
774
                'parentId' => $locationCreateStruct->parentLocationId,
775
            ]
776
        );
777
    }
778
779
    /**
780
     * Checks if given $sortField value is one of the defined sort field constants.
781
     *
782
     * @param mixed $sortField
783
     *
784
     * @return bool
785
     */
786
    public function isValidLocationSortField($sortField)
787
    {
788
        switch ($sortField) {
789
            case APILocation::SORT_FIELD_PATH:
790
            case APILocation::SORT_FIELD_PUBLISHED:
791
            case APILocation::SORT_FIELD_MODIFIED:
792
            case APILocation::SORT_FIELD_SECTION:
793
            case APILocation::SORT_FIELD_DEPTH:
794
            case APILocation::SORT_FIELD_CLASS_IDENTIFIER:
795
            case APILocation::SORT_FIELD_CLASS_NAME:
796
            case APILocation::SORT_FIELD_PRIORITY:
797
            case APILocation::SORT_FIELD_NAME:
798
            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...
799
            case APILocation::SORT_FIELD_NODE_ID:
800
            case APILocation::SORT_FIELD_CONTENTOBJECT_ID:
801
                return true;
802
        }
803
804
        return false;
805
    }
806
807
    /**
808
     * Checks if given $sortOrder value is one of the defined sort order constants.
809
     *
810
     * @param mixed $sortOrder
811
     *
812
     * @return bool
813
     */
814
    public function isValidLocationSortOrder($sortOrder)
815
    {
816
        switch ($sortOrder) {
817
            case APILocation::SORT_ORDER_DESC:
818
            case APILocation::SORT_ORDER_ASC:
819
                return true;
820
        }
821
822
        return false;
823
    }
824
825
    /**
826
     * Checks if given $priority is valid.
827
     *
828
     * @param int $priority
829
     *
830
     * @return bool
831
     */
832
    public function isValidLocationPriority($priority)
833
    {
834
        if ($priority === null) {
835
            return true;
836
        }
837
838
        return is_int($priority) && $priority >= self::MIN_LOCATION_PRIORITY && $priority <= self::MAX_LOCATION_PRIORITY;
839
    }
840
841
    /**
842
     * Validates given translated list $list, which should be an array of strings with language codes as keys.
843
     *
844
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
845
     *
846
     * @param mixed $list
847
     * @param string $argumentName
848
     */
849
    public function validateTranslatedList($list, $argumentName)
850
    {
851
        if (!is_array($list)) {
852
            throw new InvalidArgumentType($argumentName, 'array', $list);
853
        }
854
855
        foreach ($list as $languageCode => $translation) {
856
            $this->contentLanguageHandler->loadByLanguageCode($languageCode);
857
858
            if (!is_string($translation)) {
859
                throw new InvalidArgumentType($argumentName . "['$languageCode']", 'string', $translation);
860
            }
861
        }
862
    }
863
864
    /**
865
     * Returns \DateTime object from given $timestamp in environment timezone.
866
     *
867
     * This method is needed because constructing \DateTime with $timestamp will
868
     * return the object in UTC timezone.
869
     *
870
     * @param int $timestamp
871
     *
872
     * @return \DateTime
873
     */
874
    public function getDateTime($timestamp)
875
    {
876
        $dateTime = new DateTime();
877
        $dateTime->setTimestamp($timestamp);
878
879
        return $dateTime;
880
    }
881
882
    /**
883
     * Creates unique hash string for given $object.
884
     *
885
     * Used for remoteId.
886
     *
887
     * @param object $object
888
     *
889
     * @return string
890
     */
891
    public function getUniqueHash($object)
892
    {
893
        return md5(uniqid(get_class($object), true));
894
    }
895
896
    /**
897
     * Returns true if given location is a tree root.
898
     *
899
     * @param \eZ\Publish\SPI\Persistence\Content\Location $spiLocation
900
     *
901
     * @return bool
902
     */
903
    private function isRootLocation(SPILocation $spiLocation): bool
904
    {
905
        return $spiLocation->id === $spiLocation->parentId;
906
    }
907
}
908