Completed
Push — EZP-30796 ( 51655e )
by
unknown
20:53
created

buildContentDomainObjectFromPersistence()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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