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