Completed
Push — qa-cumulative-ezp-31278-31279-... ( cb92e6...b59938 )
by
unknown
21:37
created

ContentDomainMapper::buildVersionInfoDomainObject()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 12
nop 2
dl 0
loc 42
rs 8.6257
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\Core\Repository\ProxyFactory\ProxyDomainMapperInterface;
13
use eZ\Publish\SPI\Persistence\Content\Handler as ContentHandler;
14
use eZ\Publish\SPI\Persistence\Content\Location\Handler as LocationHandler;
15
use eZ\Publish\SPI\Persistence\Content\Language\Handler as LanguageHandler;
16
use eZ\Publish\SPI\Persistence\Content\Type\Handler as TypeHandler;
17
use eZ\Publish\Core\Repository\Values\Content\Content;
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 extends ProxyAwareDomainMapper
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
    public function __construct(
72
        ContentHandler $contentHandler,
73
        LocationHandler $locationHandler,
74
        TypeHandler $contentTypeHandler,
75
        ContentTypeDomainMapper $contentTypeDomainMapper,
76
        LanguageHandler $contentLanguageHandler,
77
        FieldTypeRegistry $fieldTypeRegistry,
78
        ThumbnailStrategy $thumbnailStrategy,
79
        ?ProxyDomainMapperInterface $proxyFactory = null
80
    ) {
81
        $this->contentHandler = $contentHandler;
82
        $this->locationHandler = $locationHandler;
83
        $this->contentTypeHandler = $contentTypeHandler;
84
        $this->contentTypeDomainMapper = $contentTypeDomainMapper;
85
        $this->contentLanguageHandler = $contentLanguageHandler;
86
        $this->fieldTypeRegistry = $fieldTypeRegistry;
87
        $this->thumbnailStrategy = $thumbnailStrategy;
88
        parent::__construct($proxyFactory);
89
    }
90
91
    /**
92
     * Builds a Content domain object from value object.
93
     *
94
     * @param \eZ\Publish\SPI\Persistence\Content $spiContent
95
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
96
     * @param array $prioritizedLanguages Prioritized language codes to filter fields on
97
     * @param string|null $fieldAlwaysAvailableLanguage Language code fallback if a given field is not found in $prioritizedLanguages
98
     *
99
     * @return \eZ\Publish\Core\Repository\Values\Content\Content
100
     */
101
    public function buildContentDomainObject(
102
        SPIContent $spiContent,
103
        ContentType $contentType,
104
        array $prioritizedLanguages = [],
105
        string $fieldAlwaysAvailableLanguage = null
106
    ) {
107
        $prioritizedFieldLanguageCode = null;
108
        if (!empty($prioritizedLanguages)) {
109
            $availableFieldLanguageMap = array_fill_keys($spiContent->versionInfo->languageCodes, true);
110
            foreach ($prioritizedLanguages as $prioritizedLanguage) {
111
                if (isset($availableFieldLanguageMap[$prioritizedLanguage])) {
112
                    $prioritizedFieldLanguageCode = $prioritizedLanguage;
113
                    break;
114
                }
115
            }
116
        }
117
118
        $internalFields = $this->buildDomainFields($spiContent->fields, $contentType, $prioritizedLanguages, $fieldAlwaysAvailableLanguage);
119
120
        return new Content(
121
            [
122
                'thumbnail' => $this->thumbnailStrategy->getThumbnail($contentType, $internalFields),
123
                'internalFields' => $internalFields,
124
                'versionInfo' => $this->buildVersionInfoDomainObject($spiContent->versionInfo, $prioritizedLanguages),
125
                'contentType' => $contentType,
126
                'prioritizedFieldLanguageCode' => $prioritizedFieldLanguageCode,
127
            ]
128
        );
129
    }
130
131
    /**
132
     * Builds a Content domain object from value object returned from persistence.
133
     *
134
     * @param \eZ\Publish\SPI\Persistence\Content $spiContent
135
     * @param \eZ\Publish\SPI\Persistence\Content\Type $spiContentType
136
     * @param string[] $prioritizedLanguages Prioritized language codes to filter fields on
137
     * @param string|null $fieldAlwaysAvailableLanguage Language code fallback if a given field is not found in $prioritizedLanguages
138
     *
139
     * @return \eZ\Publish\Core\Repository\Values\Content\Content
140
     */
141
    public function buildContentDomainObjectFromPersistence(
142
        SPIContent $spiContent,
143
        SPIContentType $spiContentType,
144
        array $prioritizedLanguages = [],
145
        ?string $fieldAlwaysAvailableLanguage = null
146
    ): APIContent {
147
        $contentType = $this->contentTypeDomainMapper->buildContentTypeDomainObject($spiContentType, $prioritizedLanguages);
148
149
        return $this->buildContentDomainObject($spiContent, $contentType, $prioritizedLanguages, $fieldAlwaysAvailableLanguage);
150
    }
151
152
    /**
153
     * Builds a Content proxy object (lazy loaded, loads as soon as used).
154
     */
155
    public function buildContentProxy(
156
        SPIContent\ContentInfo $info,
157
        array $prioritizedLanguages = [],
158
        bool $useAlwaysAvailable = true
159
    ): APIContent {
160
        return $this->proxyFactory->createContentProxy(
161
            $info->id,
162
            $prioritizedLanguages,
163
            $useAlwaysAvailable
164
        );
165
    }
166
167
    /**
168
     * Builds a list of Content proxy objects (lazy loaded, loads all as soon as one of them loads).
169
     *
170
     * @param \eZ\Publish\SPI\Persistence\Content\ContentInfo[] $infoList
171
     * @param string[] $prioritizedLanguages
172
     * @param bool $useAlwaysAvailable
173
     *
174
     * @return \eZ\Publish\API\Repository\Values\Content\Content[<int>]
175
     */
176
    public function buildContentProxyList(
177
        array $infoList,
178
        array $prioritizedLanguages = [],
179
        bool $useAlwaysAvailable = true
180
    ): array {
181
        $list = [];
182
        foreach ($infoList as $info) {
183
            $list[$info->id] = $this->proxyFactory->createContentProxy(
184
                $info->id,
185
                $prioritizedLanguages,
186
                $useAlwaysAvailable
187
            );
188
        }
189
190
        return $list;
191
    }
192
193
    /**
194
     * Returns an array of domain fields created from given array of SPI fields.
195
     *
196
     * @throws InvalidArgumentType On invalid $contentType
197
     *
198
     * @param \eZ\Publish\SPI\Persistence\Content\Field[] $spiFields
199
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType|\eZ\Publish\SPI\Persistence\Content\Type $contentType
200
     * @param array $prioritizedLanguages A language priority, filters returned fields and is used as prioritized language code on
201
     *                         returned value object. If not given all languages are returned.
202
     * @param string|null $alwaysAvailableLanguage Language code fallback if a given field is not found in $prioritizedLanguages
203
     *
204
     * @return array
205
     */
206
    public function buildDomainFields(
207
        array $spiFields,
208
        $contentType,
209
        array $prioritizedLanguages = [],
210
        string $alwaysAvailableLanguage = null
211
    ) {
212
        if (!$contentType instanceof SPIContentType && !$contentType instanceof ContentType) {
213
            throw new InvalidArgumentType('$contentType', 'SPI ContentType | API ContentType');
214
        }
215
216
        $fieldDefinitionsMap = [];
217
        foreach ($contentType->fieldDefinitions as $fieldDefinition) {
218
            $fieldDefinitionsMap[$fieldDefinition->id] = $fieldDefinition;
219
        }
220
221
        $fieldInFilterLanguagesMap = [];
222
        if (!empty($prioritizedLanguages) && $alwaysAvailableLanguage !== null) {
223
            foreach ($spiFields as $spiField) {
224
                if (in_array($spiField->languageCode, $prioritizedLanguages)) {
225
                    $fieldInFilterLanguagesMap[$spiField->fieldDefinitionId] = true;
226
                }
227
            }
228
        }
229
230
        $fields = [];
231
        foreach ($spiFields as $spiField) {
232
            // We ignore fields in content not part of the content type
233
            if (!isset($fieldDefinitionsMap[$spiField->fieldDefinitionId])) {
234
                continue;
235
            }
236
237
            $fieldDefinition = $fieldDefinitionsMap[$spiField->fieldDefinitionId];
238
239
            if (!empty($prioritizedLanguages) && !in_array($spiField->languageCode, $prioritizedLanguages)) {
240
                // If filtering is enabled we ignore fields in other languages then $prioritizedLanguages, if:
241
                if ($alwaysAvailableLanguage === null) {
242
                    // Ignore field if we don't have $alwaysAvailableLanguageCode fallback
243
                    continue;
244
                } elseif (!empty($fieldInFilterLanguagesMap[$spiField->fieldDefinitionId])) {
245
                    // Ignore field if it exists in one of the filtered languages
246
                    continue;
247
                } elseif ($spiField->languageCode !== $alwaysAvailableLanguage) {
248
                    // Also ignore if field is not in $alwaysAvailableLanguageCode
249
                    continue;
250
                }
251
            }
252
253
            $fields[$fieldDefinition->position][] = new Field(
254
                [
255
                    'id' => $spiField->id,
256
                    'value' => $this->fieldTypeRegistry->getFieldType($spiField->type)
257
                        ->fromPersistenceValue($spiField->value),
258
                    'languageCode' => $spiField->languageCode,
259
                    'fieldDefIdentifier' => $fieldDefinition->identifier,
260
                    'fieldTypeIdentifier' => $spiField->type,
261
                ]
262
            );
263
        }
264
265
        // Sort fields by content type field definition priority
266
        ksort($fields, SORT_NUMERIC);
267
268
        // Flatten array
269
        return array_merge(...$fields);
270
    }
271
272
    /**
273
     * Builds a VersionInfo domain object from value object returned from persistence.
274
     *
275
     * @param \eZ\Publish\SPI\Persistence\Content\VersionInfo $spiVersionInfo
276
     * @param array $prioritizedLanguages
277
     *
278
     * @return \eZ\Publish\Core\Repository\Values\Content\VersionInfo
279
     */
280
    public function buildVersionInfoDomainObject(SPIVersionInfo $spiVersionInfo, array $prioritizedLanguages = [])
281
    {
282
        // Map SPI statuses to API
283
        switch ($spiVersionInfo->status) {
284
            case SPIVersionInfo::STATUS_ARCHIVED:
285
                $status = APIVersionInfo::STATUS_ARCHIVED;
286
                break;
287
288
            case SPIVersionInfo::STATUS_PUBLISHED:
289
                $status = APIVersionInfo::STATUS_PUBLISHED;
290
                break;
291
292
            case SPIVersionInfo::STATUS_DRAFT:
293
            default:
294
                $status = APIVersionInfo::STATUS_DRAFT;
295
        }
296
297
        // Find prioritised language among names
298
        $prioritizedNameLanguageCode = null;
299
        foreach ($prioritizedLanguages as $prioritizedLanguage) {
300
            if (isset($spiVersionInfo->names[$prioritizedLanguage])) {
301
                $prioritizedNameLanguageCode = $prioritizedLanguage;
302
                break;
303
            }
304
        }
305
306
        return new VersionInfo(
307
            [
308
                'id' => $spiVersionInfo->id,
309
                'versionNo' => $spiVersionInfo->versionNo,
310
                'modificationDate' => $this->getDateTime($spiVersionInfo->modificationDate),
311
                'creatorId' => $spiVersionInfo->creatorId,
312
                'creationDate' => $this->getDateTime($spiVersionInfo->creationDate),
313
                'status' => $status,
314
                'initialLanguageCode' => $spiVersionInfo->initialLanguageCode,
315
                'languageCodes' => $spiVersionInfo->languageCodes,
316
                'names' => $spiVersionInfo->names,
317
                'contentInfo' => $this->buildContentInfoDomainObject($spiVersionInfo->contentInfo),
318
                'prioritizedNameLanguageCode' => $prioritizedNameLanguageCode,
319
                'creator' => $this->proxyFactory->createUserProxy($spiVersionInfo->creatorId, $prioritizedLanguages),
320
                'initialLanguage' => $this->proxyFactory->createLanguageProxy($spiVersionInfo->initialLanguageCode),
321
                'languages' => $this->proxyFactory->createLanguageProxyList($spiVersionInfo->languageCodes),
322
            ]
323
        );
324
    }
325
326
    /**
327
     * Builds a ContentInfo domain object from value object returned from persistence.
328
     *
329
     * @param \eZ\Publish\SPI\Persistence\Content\ContentInfo $spiContentInfo
330
     *
331
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
332
     */
333
    public function buildContentInfoDomainObject(SPIContentInfo $spiContentInfo)
334
    {
335
        // Map SPI statuses to API
336
        switch ($spiContentInfo->status) {
337
            case SPIContentInfo::STATUS_TRASHED:
338
                $status = ContentInfo::STATUS_TRASHED;
339
                break;
340
341
            case SPIContentInfo::STATUS_PUBLISHED:
342
                $status = ContentInfo::STATUS_PUBLISHED;
343
                break;
344
345
            case SPIContentInfo::STATUS_DRAFT:
346
            default:
347
                $status = ContentInfo::STATUS_DRAFT;
348
        }
349
350
        return new ContentInfo(
351
            [
352
                'id' => $spiContentInfo->id,
353
                'contentTypeId' => $spiContentInfo->contentTypeId,
354
                'name' => $spiContentInfo->name,
355
                'sectionId' => $spiContentInfo->sectionId,
356
                'currentVersionNo' => $spiContentInfo->currentVersionNo,
357
                'published' => $spiContentInfo->isPublished,
358
                'ownerId' => $spiContentInfo->ownerId,
359
                'modificationDate' => $spiContentInfo->modificationDate == 0 ?
360
                    null :
361
                    $this->getDateTime($spiContentInfo->modificationDate),
362
                'publishedDate' => $spiContentInfo->publicationDate == 0 ?
363
                    null :
364
                    $this->getDateTime($spiContentInfo->publicationDate),
365
                'alwaysAvailable' => $spiContentInfo->alwaysAvailable,
366
                'remoteId' => $spiContentInfo->remoteId,
367
                'mainLanguageCode' => $spiContentInfo->mainLanguageCode,
368
                'mainLocationId' => $spiContentInfo->mainLocationId,
369
                'status' => $status,
370
                'isHidden' => $spiContentInfo->isHidden,
371
                'contentType' => $this->proxyFactory->createContentTypeProxy($spiContentInfo->contentTypeId),
372
                'section' => $this->proxyFactory->createSectionProxy($spiContentInfo->sectionId),
373
                'mainLocation' => $spiContentInfo->mainLocationId !== null ? $this->proxyFactory->createLocationProxy($spiContentInfo->mainLocationId) : null,
374
                'mainLanguage' => $this->proxyFactory->createLanguageProxy($spiContentInfo->mainLanguageCode),
375
                'owner' => $this->proxyFactory->createUserProxy($spiContentInfo->ownerId),
376
            ]
377
        );
378
    }
379
380
    /**
381
     * Builds API Relation object from provided SPI Relation object.
382
     *
383
     * @param \eZ\Publish\SPI\Persistence\Content\Relation $spiRelation
384
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $sourceContentInfo
385
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $destinationContentInfo
386
     *
387
     * @return \eZ\Publish\API\Repository\Values\Content\Relation
388
     */
389
    public function buildRelationDomainObject(
390
        SPIRelation $spiRelation,
391
        ContentInfo $sourceContentInfo,
392
        ContentInfo $destinationContentInfo
393
    ) {
394
        $sourceFieldDefinitionIdentifier = null;
395
        if ($spiRelation->sourceFieldDefinitionId !== null) {
396
            $contentType = $this->contentTypeHandler->load($sourceContentInfo->contentTypeId);
397
            foreach ($contentType->fieldDefinitions as $fieldDefinition) {
398
                if ($fieldDefinition->id !== $spiRelation->sourceFieldDefinitionId) {
399
                    continue;
400
                }
401
402
                $sourceFieldDefinitionIdentifier = $fieldDefinition->identifier;
403
                break;
404
            }
405
        }
406
407
        return new Relation(
408
            [
409
                'id' => $spiRelation->id,
410
                'sourceFieldDefinitionIdentifier' => $sourceFieldDefinitionIdentifier,
411
                'type' => $spiRelation->type,
412
                'sourceContentInfo' => $sourceContentInfo,
413
                'destinationContentInfo' => $destinationContentInfo,
414
            ]
415
        );
416
    }
417
418
    /**
419
     * @deprecated Since 7.2, use buildLocationWithContent(), buildLocation() or (private) mapLocation() instead.
420
     */
421
    public function buildLocationDomainObject(
422
        SPILocation $spiLocation,
423
        SPIContentInfo $contentInfo = null
424
    ) {
425
        if ($contentInfo === null) {
426
            return $this->buildLocation($spiLocation);
427
        }
428
429
        return $this->mapLocation(
430
            $spiLocation,
431
            $this->buildContentInfoDomainObject($contentInfo),
432
            $this->buildContentProxy($contentInfo)
433
        );
434
    }
435
436
    public function buildLocation(
437
        SPILocation $spiLocation,
438
        array $prioritizedLanguages = [],
439
        bool $useAlwaysAvailable = true
440
    ): APILocation {
441
        if ($this->isRootLocation($spiLocation)) {
442
            return $this->buildRootLocation($spiLocation);
443
        }
444
445
        $spiContentInfo = $this->contentHandler->loadContentInfo($spiLocation->contentId);
446
447
        return $this->mapLocation(
448
            $spiLocation,
449
            $this->buildContentInfoDomainObject($spiContentInfo),
450
            $this->buildContentProxy($spiContentInfo, $prioritizedLanguages, $useAlwaysAvailable),
451
            $this->proxyFactory->createLocationProxy($spiLocation->parentId, $prioritizedLanguages)
452
        );
453
    }
454
455
    /**
456
     * @param \eZ\Publish\SPI\Persistence\Content\Location $spiLocation
457
     * @param \eZ\Publish\API\Repository\Values\Content\Content|null $content
458
     * @param \eZ\Publish\SPI\Persistence\Content\ContentInfo|null $spiContentInfo
459
     *
460
     * @return \eZ\Publish\API\Repository\Values\Content\Location
461
     *
462
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
463
     */
464
    public function buildLocationWithContent(
465
        SPILocation $spiLocation,
466
        ?APIContent $content,
467
        ?SPIContentInfo $spiContentInfo = null
468
    ): APILocation {
469
        if ($this->isRootLocation($spiLocation)) {
470
            return $this->buildRootLocation($spiLocation);
471
        }
472
473
        if ($content === null) {
474
            throw new InvalidArgumentException('$content', "Location {$spiLocation->id} has missing Content");
475
        }
476
477
        if ($spiContentInfo !== null) {
478
            $contentInfo = $this->buildContentInfoDomainObject($spiContentInfo);
479
        } else {
480
            $contentInfo = $content->contentInfo;
481
        }
482
483
        $parentLocation = $this->proxyFactory->createLocationProxy(
484
            $spiLocation->parentId,
485
        );
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected ')'
Loading history...
486
487
        return $this->mapLocation($spiLocation, $contentInfo, $content, $parentLocation);
488
    }
489
490
    /**
491
     * Builds API Location object for tree root.
492
     *
493
     * @param \eZ\Publish\SPI\Persistence\Content\Location $spiLocation
494
     *
495
     * @return \eZ\Publish\API\Repository\Values\Content\Location
496
     */
497
    private function buildRootLocation(SPILocation $spiLocation): APILocation
498
    {
499
        //  first known commit of eZ Publish 3.x
500
        $legacyDateTime = $this->getDateTime(1030968000);
501
502
        $contentInfo = new ContentInfo([
503
            'id' => 0,
504
            'name' => 'Top Level Nodes',
505
            'sectionId' => 1,
506
            'mainLocationId' => 1,
507
            'contentTypeId' => 1,
508
            'currentVersionNo' => 1,
509
            'published' => 1,
510
            'ownerId' => 14, // admin user
511
            'modificationDate' => $legacyDateTime,
512
            'publishedDate' => $legacyDateTime,
513
            'alwaysAvailable' => 1,
514
            'remoteId' => null,
515
            'mainLanguageCode' => 'eng-GB',
516
        ]);
517
518
        $content = new Content([
519
            'versionInfo' => new VersionInfo([
520
                'names' => [
521
                    $contentInfo->mainLanguageCode => $contentInfo->name,
522
                ],
523
                'contentInfo' => $contentInfo,
524
                'versionNo' => $contentInfo->currentVersionNo,
525
                'modificationDate' => $contentInfo->modificationDate,
526
                'creationDate' => $contentInfo->modificationDate,
527
                'creatorId' => $contentInfo->ownerId,
528
            ]),
529
        ]);
530
531
        // NOTE: this is hardcoded workaround for missing ContentInfo on root location
532
        return $this->mapLocation(
533
            $spiLocation,
534
            $contentInfo,
535
            $content
536
        );
537
    }
538
539
    private function mapLocation(
540
        SPILocation $spiLocation,
541
        ContentInfo $contentInfo,
542
        APIContent $content,
543
        ?APILocation $parentLocation = null
544
    ): APILocation {
545
        return new Location(
546
            [
547
                'content' => $content,
548
                'contentInfo' => $contentInfo,
549
                'id' => $spiLocation->id,
550
                'priority' => $spiLocation->priority,
551
                'hidden' => $spiLocation->hidden || $contentInfo->isHidden,
552
                'invisible' => $spiLocation->invisible,
553
                'explicitlyHidden' => $spiLocation->hidden,
554
                'remoteId' => $spiLocation->remoteId,
555
                'parentLocationId' => $spiLocation->parentId,
556
                'pathString' => $spiLocation->pathString,
557
                'depth' => $spiLocation->depth,
558
                'sortField' => $spiLocation->sortField,
559
                'sortOrder' => $spiLocation->sortOrder,
560
                'parentLocation' => $parentLocation,
561
            ]
562
        );
563
    }
564
565
    /**
566
     * Build API Content domain objects in bulk and apply to ContentSearchResult.
567
     *
568
     * Loading of Content objects are done in bulk.
569
     *
570
     * @param \eZ\Publish\API\Repository\Values\Content\Search\SearchResult $result SPI search result with SPI ContentInfo items as hits
571
     * @param array $languageFilter
572
     *
573
     * @return \eZ\Publish\SPI\Persistence\Content\ContentInfo[] ContentInfo we did not find content for is returned.
574
     */
575
    public function buildContentDomainObjectsOnSearchResult(SearchResult $result, array $languageFilter)
576
    {
577
        if (empty($result->searchHits)) {
578
            return [];
579
        }
580
581
        $contentIds = [];
582
        $contentTypeIds = [];
583
        $translations = $languageFilter['languages'] ?? [];
584
        $useAlwaysAvailable = $languageFilter['useAlwaysAvailable'] ?? true;
585
        foreach ($result->searchHits as $hit) {
586
            /** @var \eZ\Publish\SPI\Persistence\Content\ContentInfo $info */
587
            $info = $hit->valueObject;
588
            $contentIds[] = $info->id;
589
            $contentTypeIds[] = $info->contentTypeId;
590
            // Unless we are told to load all languages, we add main language to translations so they are loaded too
591
            // Might in some case load more languages then intended, but prioritised handling will pick right one
592
            if (!empty($languageFilter['languages']) && $useAlwaysAvailable && $info->alwaysAvailable) {
593
                $translations[] = $info->mainLanguageCode;
594
            }
595
        }
596
597
        $missingContentList = [];
598
        $contentList = $this->contentHandler->loadContentList($contentIds, array_unique($translations));
599
        $contentTypeList = $this->contentTypeHandler->loadContentTypeList(array_unique($contentTypeIds));
600
        foreach ($result->searchHits as $key => $hit) {
601
            if (isset($contentList[$hit->valueObject->id])) {
602
                $hit->valueObject = $this->buildContentDomainObject(
603
                    $contentList[$hit->valueObject->id],
604
                    $this->contentTypeDomainMapper->buildContentTypeDomainObject(
605
                        $contentTypeList[$hit->valueObject->contentTypeId],
606
                        $languageFilter['languages'] ?? []
607
                    ),
608
                    $languageFilter['languages'] ?? [],
609
                    $useAlwaysAvailable ? $hit->valueObject->mainLanguageCode : null
610
                );
611
            } else {
612
                $missingContentList[] = $hit->valueObject;
613
                unset($result->searchHits[$key]);
614
                --$result->totalCount;
615
            }
616
        }
617
618
        return $missingContentList;
619
    }
620
621
    /**
622
     * Build API Location and corresponding ContentInfo domain objects and apply to LocationSearchResult.
623
     *
624
     * This is done in order to be able to:
625
     * Load ContentInfo objects in bulk, generate proxy objects for Content that will loaded in bulk on-demand (on use).
626
     *
627
     * @param \eZ\Publish\API\Repository\Values\Content\Search\SearchResult $result SPI search result with SPI Location items as hits
628
     * @param array $languageFilter
629
     *
630
     * @return \eZ\Publish\SPI\Persistence\Content\Location[] Locations we did not find content info for is returned.
631
     */
632
    public function buildLocationDomainObjectsOnSearchResult(SearchResult $result, array $languageFilter)
633
    {
634
        if (empty($result->searchHits)) {
635
            return [];
636
        }
637
638
        $contentIds = [];
639
        foreach ($result->searchHits as $hit) {
640
            $contentIds[] = $hit->valueObject->contentId;
641
        }
642
643
        $missingLocations = [];
644
        $contentInfoList = $this->contentHandler->loadContentInfoList($contentIds);
645
        $contentList = $this->buildContentProxyList(
646
            $contentInfoList,
647
            !empty($languageFilter['languages']) ? $languageFilter['languages'] : []
648
        );
649
        foreach ($result->searchHits as $key => $hit) {
650
            if (isset($contentInfoList[$hit->valueObject->contentId])) {
651
                $hit->valueObject = $this->buildLocationWithContent(
652
                    $hit->valueObject,
653
                    $contentList[$hit->valueObject->contentId],
654
                    $contentInfoList[$hit->valueObject->contentId]
655
                );
656
            } else {
657
                $missingLocations[] = $hit->valueObject;
658
                unset($result->searchHits[$key]);
659
                --$result->totalCount;
660
            }
661
        }
662
663
        return $missingLocations;
664
    }
665
666
    /**
667
     * Creates an array of SPI location create structs from given array of API location create structs.
668
     *
669
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
670
     *
671
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct $locationCreateStruct
672
     * @param \eZ\Publish\API\Repository\Values\Content\Location $parentLocation
673
     * @param mixed $mainLocation
674
     * @param mixed $contentId
675
     * @param mixed $contentVersionNo
676
     *
677
     * @return \eZ\Publish\SPI\Persistence\Content\Location\CreateStruct
678
     */
679
    public function buildSPILocationCreateStruct(
680
        $locationCreateStruct,
681
        APILocation $parentLocation,
682
        $mainLocation,
683
        $contentId,
684
        $contentVersionNo
685
    ) {
686
        if (!$this->isValidLocationPriority($locationCreateStruct->priority)) {
687
            throw new InvalidArgumentValue('priority', $locationCreateStruct->priority, 'LocationCreateStruct');
688
        }
689
690
        if (!is_bool($locationCreateStruct->hidden)) {
691
            throw new InvalidArgumentValue('hidden', $locationCreateStruct->hidden, 'LocationCreateStruct');
692
        }
693
694
        if ($locationCreateStruct->remoteId !== null && (!is_string($locationCreateStruct->remoteId) || empty($locationCreateStruct->remoteId))) {
695
            throw new InvalidArgumentValue('remoteId', $locationCreateStruct->remoteId, 'LocationCreateStruct');
696
        }
697
698
        if ($locationCreateStruct->sortField !== null && !$this->isValidLocationSortField($locationCreateStruct->sortField)) {
699
            throw new InvalidArgumentValue('sortField', $locationCreateStruct->sortField, 'LocationCreateStruct');
700
        }
701
702
        if ($locationCreateStruct->sortOrder !== null && !$this->isValidLocationSortOrder($locationCreateStruct->sortOrder)) {
703
            throw new InvalidArgumentValue('sortOrder', $locationCreateStruct->sortOrder, 'LocationCreateStruct');
704
        }
705
706
        $remoteId = $locationCreateStruct->remoteId;
707
        if (null === $remoteId) {
708
            $remoteId = $this->getUniqueHash($locationCreateStruct);
709
        } else {
710
            try {
711
                $this->locationHandler->loadByRemoteId($remoteId);
712
                throw new InvalidArgumentException(
713
                    '$locationCreateStructs',
714
                    "Another Location with remoteId '{$remoteId}' exists"
715
                );
716
            } catch (NotFoundException $e) {
717
                // Do nothing
718
            }
719
        }
720
721
        return new SPILocationCreateStruct(
722
            [
723
                'priority' => $locationCreateStruct->priority,
724
                'hidden' => $locationCreateStruct->hidden,
725
                // If we declare the new Location as hidden, it is automatically invisible
726
                // Otherwise it picks up visibility from parent Location
727
                // Note: There is no need to check for hidden status of parent, as hidden Location
728
                // is always invisible as well
729
                'invisible' => ($locationCreateStruct->hidden === true || $parentLocation->invisible),
730
                'remoteId' => $remoteId,
731
                'contentId' => $contentId,
732
                'contentVersion' => $contentVersionNo,
733
                // pathIdentificationString will be set in storage
734
                'pathIdentificationString' => null,
735
                'mainLocationId' => $mainLocation,
736
                'sortField' => $locationCreateStruct->sortField !== null ? $locationCreateStruct->sortField : Location::SORT_FIELD_NAME,
737
                'sortOrder' => $locationCreateStruct->sortOrder !== null ? $locationCreateStruct->sortOrder : Location::SORT_ORDER_ASC,
738
                'parentId' => $locationCreateStruct->parentLocationId,
739
            ]
740
        );
741
    }
742
743
    /**
744
     * Checks if given $sortField value is one of the defined sort field constants.
745
     *
746
     * @param mixed $sortField
747
     *
748
     * @return bool
749
     */
750
    public function isValidLocationSortField($sortField)
751
    {
752
        switch ($sortField) {
753
            case APILocation::SORT_FIELD_PATH:
754
            case APILocation::SORT_FIELD_PUBLISHED:
755
            case APILocation::SORT_FIELD_MODIFIED:
756
            case APILocation::SORT_FIELD_SECTION:
757
            case APILocation::SORT_FIELD_DEPTH:
758
            case APILocation::SORT_FIELD_CLASS_IDENTIFIER:
759
            case APILocation::SORT_FIELD_CLASS_NAME:
760
            case APILocation::SORT_FIELD_PRIORITY:
761
            case APILocation::SORT_FIELD_NAME:
762
            case APILocation::SORT_FIELD_MODIFIED_SUBNODE:
763
            case APILocation::SORT_FIELD_NODE_ID:
764
            case APILocation::SORT_FIELD_CONTENTOBJECT_ID:
765
                return true;
766
        }
767
768
        return false;
769
    }
770
771
    /**
772
     * Checks if given $sortOrder value is one of the defined sort order constants.
773
     *
774
     * @param mixed $sortOrder
775
     *
776
     * @return bool
777
     */
778
    public function isValidLocationSortOrder($sortOrder)
779
    {
780
        switch ($sortOrder) {
781
            case APILocation::SORT_ORDER_DESC:
782
            case APILocation::SORT_ORDER_ASC:
783
                return true;
784
        }
785
786
        return false;
787
    }
788
789
    /**
790
     * Checks if given $priority is valid.
791
     *
792
     * @param int $priority
793
     *
794
     * @return bool
795
     */
796
    public function isValidLocationPriority($priority)
797
    {
798
        if ($priority === null) {
799
            return true;
800
        }
801
802
        return is_int($priority) && $priority >= self::MIN_LOCATION_PRIORITY && $priority <= self::MAX_LOCATION_PRIORITY;
803
    }
804
805
    /**
806
     * Validates given translated list $list, which should be an array of strings with language codes as keys.
807
     *
808
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
809
     *
810
     * @param mixed $list
811
     * @param string $argumentName
812
     */
813
    public function validateTranslatedList($list, $argumentName)
814
    {
815
        if (!is_array($list)) {
816
            throw new InvalidArgumentType($argumentName, 'array', $list);
817
        }
818
819
        foreach ($list as $languageCode => $translation) {
820
            $this->contentLanguageHandler->loadByLanguageCode($languageCode);
821
822
            if (!is_string($translation)) {
823
                throw new InvalidArgumentType($argumentName . "['$languageCode']", 'string', $translation);
824
            }
825
        }
826
    }
827
828
    /**
829
     * Returns \DateTime object from given $timestamp in environment timezone.
830
     *
831
     * This method is needed because constructing \DateTime with $timestamp will
832
     * return the object in UTC timezone.
833
     *
834
     * @param int $timestamp
835
     *
836
     * @return \DateTime
837
     */
838
    public function getDateTime($timestamp)
839
    {
840
        $dateTime = new DateTime();
841
        $dateTime->setTimestamp($timestamp);
842
843
        return $dateTime;
844
    }
845
846
    /**
847
     * Creates unique hash string for given $object.
848
     *
849
     * Used for remoteId.
850
     *
851
     * @param object $object
852
     *
853
     * @return string
854
     */
855
    public function getUniqueHash($object)
856
    {
857
        return md5(uniqid(get_class($object), true));
858
    }
859
860
    /**
861
     * Returns true if given location is a tree root.
862
     *
863
     * @param \eZ\Publish\SPI\Persistence\Content\Location $spiLocation
864
     *
865
     * @return bool
866
     */
867
    private function isRootLocation(SPILocation $spiLocation): bool
868
    {
869
        return $spiLocation->id === $spiLocation->parentId;
870
    }
871
}
872