vendor/ezsystems/ezpublish-kernel/eZ/Publish/Core/Repository/Helper/DomainMapper.php line 226

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