vendor/ezsystems/ezplatform-kernel/eZ/Publish/Core/Repository/Mapper/ContentDomainMapper.php line 45

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