Completed
Push — master ( 31552d...955b04 )
by
unknown
56:33 queued 38:08
created

DomainMapper::isRootLocation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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