Completed
Push — ezp_30981_content_info_proxy ( e47d06...65f6d5 )
by
unknown
19:59
created

DomainMapper::buildVersionInfoDomainObject()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

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