vendor/sulu/article-bundle/Content/ArticleDataProvider.php line 257

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of Sulu.
  4.  *
  5.  * (c) Sulu GmbH
  6.  *
  7.  * This source file is subject to the MIT license that is bundled
  8.  * with this source code in the file LICENSE.
  9.  */
  10. namespace Sulu\Bundle\ArticleBundle\Content;
  11. use ONGR\ElasticsearchBundle\Service\Manager;
  12. use ONGR\ElasticsearchDSL\Query\Compound\BoolQuery;
  13. use ONGR\ElasticsearchDSL\Query\MatchAllQuery;
  14. use ONGR\ElasticsearchDSL\Query\TermLevel\TermQuery;
  15. use ONGR\ElasticsearchDSL\Search;
  16. use ONGR\ElasticsearchDSL\Sort\FieldSort;
  17. use ProxyManager\Factory\LazyLoadingValueHolderFactory;
  18. use ProxyManager\Proxy\LazyLoadingInterface;
  19. use Sulu\Bundle\AdminBundle\Metadata\FormMetadata\TypedFormMetadata;
  20. use Sulu\Bundle\AdminBundle\Metadata\MetadataProviderInterface;
  21. use Sulu\Bundle\ArticleBundle\Document\ArticleDocument;
  22. use Sulu\Bundle\ArticleBundle\Document\ArticleViewDocumentInterface;
  23. use Sulu\Bundle\PageBundle\Content\Types\SegmentSelect;
  24. use Sulu\Bundle\WebsiteBundle\ReferenceStore\ReferenceStoreInterface;
  25. use Sulu\Component\Content\Compat\PropertyParameter;
  26. use Sulu\Component\DocumentManager\DocumentManagerInterface;
  27. use Sulu\Component\Security\Authentication\UserInterface;
  28. use Sulu\Component\SmartContent\Configuration\Builder;
  29. use Sulu\Component\SmartContent\Configuration\BuilderInterface;
  30. use Sulu\Component\SmartContent\DataProviderAliasInterface;
  31. use Sulu\Component\SmartContent\DataProviderInterface;
  32. use Sulu\Component\SmartContent\DataProviderResult;
  33. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  34. /**
  35.  * Introduces articles in smart-content.
  36.  */
  37. class ArticleDataProvider implements DataProviderInterfaceDataProviderAliasInterface
  38. {
  39.     /**
  40.      * @var Manager
  41.      */
  42.     protected $searchManager;
  43.     /**
  44.      * @var DocumentManagerInterface
  45.      */
  46.     protected $documentManager;
  47.     /**
  48.      * @var LazyLoadingValueHolderFactory
  49.      */
  50.     protected $proxyFactory;
  51.     /**
  52.      * @var ReferenceStoreInterface
  53.      */
  54.     private $referenceStore;
  55.     /**
  56.      * @var ArticleResourceItemFactory
  57.      */
  58.     protected $articleResourceItemFactory;
  59.     /**
  60.      * @var string
  61.      */
  62.     protected $articleDocumentClass;
  63.     /**
  64.      * @var int
  65.      */
  66.     protected $defaultLimit;
  67.     /**
  68.      * @var MetadataProviderInterface|null
  69.      */
  70.     private $formMetadataProvider;
  71.     /**
  72.      * @var TokenStorageInterface|null
  73.      */
  74.     private $tokenStorage;
  75.     public function __construct(
  76.         Manager $searchManager,
  77.         DocumentManagerInterface $documentManager,
  78.         LazyLoadingValueHolderFactory $proxyFactory,
  79.         ReferenceStoreInterface $referenceStore,
  80.         ArticleResourceItemFactory $articleResourceItemFactory,
  81.         string $articleDocumentClass,
  82.         int $defaultLimit,
  83.         MetadataProviderInterface $formMetadataProvider null,
  84.         TokenStorageInterface $tokenStorage null
  85.     ) {
  86.         $this->searchManager $searchManager;
  87.         $this->documentManager $documentManager;
  88.         $this->proxyFactory $proxyFactory;
  89.         $this->referenceStore $referenceStore;
  90.         $this->articleResourceItemFactory $articleResourceItemFactory;
  91.         $this->articleDocumentClass $articleDocumentClass;
  92.         $this->defaultLimit $defaultLimit;
  93.         $this->formMetadataProvider $formMetadataProvider;
  94.         $this->tokenStorage $tokenStorage;
  95.     }
  96.     public function getConfiguration()
  97.     {
  98.         return $this->getConfigurationBuilder()->getConfiguration();
  99.     }
  100.     /**
  101.      * Create new configuration-builder.
  102.      */
  103.     protected function getConfigurationBuilder(): BuilderInterface
  104.     {
  105.         $builder Builder::create()
  106.             ->enableTags()
  107.             ->enableCategories()
  108.             ->enableLimit()
  109.             ->enablePagination()
  110.             ->enablePresentAs()
  111.             ->enableSorting(
  112.                 [
  113.                     ['column' => 'published''title' => 'sulu_admin.published'],
  114.                     ['column' => 'authored''title' => 'sulu_admin.authored'],
  115.                     ['column' => 'created''title' => 'sulu_admin.created'],
  116.                     ['column' => 'title.raw''title' => 'sulu_admin.title'],
  117.                     ['column' => 'author_full_name.raw''title' => 'sulu_admin.author'],
  118.                 ]
  119.             );
  120.         if (\method_exists($builder'enableTypes')) {
  121.             $builder->enableTypes($this->getTypes());
  122.         }
  123.         return $builder;
  124.     }
  125.     public function getDefaultPropertyParameter()
  126.     {
  127.         return [
  128.             'type' => new PropertyParameter('type'null),
  129.             'ignoreWebspaces' => new PropertyParameter('ignoreWebspaces'false),
  130.         ];
  131.     }
  132.     public function resolveDataItems(
  133.         array $filters,
  134.         array $propertyParameter,
  135.         array $options = [],
  136.         $limit null,
  137.         $page 1,
  138.         $pageSize null
  139.     ) {
  140.         // there are two different kinds of types in the context of the article bundle: template-type and article-type
  141.         // filtering by article-type is possible via the types xml param
  142.         // filtering by template-type is possible via the structureTypes xml param and the admin interface overlay
  143.         // unfortunately, the admin frontend sends the selected types in $filters['types'] to the provider
  144.         // TODO: adjust the naming of the xml params to be consistent consistent, but this will be a bc break
  145.         $filters['structureTypes'] = \array_merge($filters['types'] ?? [], $this->getStructureTypesProperty($propertyParameter));
  146.         $filters['types'] = $this->getTypesProperty($propertyParameter);
  147.         $filters['excluded'] = $this->getExcludedFilter($filters$propertyParameter);
  148.         $locale $options['locale'];
  149.         $webspaceKey $this->getWebspaceKey($propertyParameter$options);
  150.         $queryResult $this->getSearchResult($filters$limit$page$pageSize$locale$webspaceKey);
  151.         $result = [];
  152.         /** @var ArticleViewDocumentInterface $document */
  153.         foreach ($queryResult as $document) {
  154.             $result[] = new ArticleDataItem($document->getUuid(), $document->getTitle(), $document);
  155.         }
  156.         return new DataProviderResult($result$this->hasNextPage($queryResult$limit$page$pageSize));
  157.     }
  158.     public function resolveResourceItems(
  159.         array $filters,
  160.         array $propertyParameter,
  161.         array $options = [],
  162.         $limit null,
  163.         $page 1,
  164.         $pageSize null
  165.     ) {
  166.         // there are two different kinds of types in the context of the article bundle: template-type and article-type
  167.         // filtering by article-type is possible via the types xml param
  168.         // filtering by template-type is possible via the structureTypes xml param and the admin interface overlay
  169.         // unfortunately, the admin frontend sends the selected types in $filters['types'] to the provider
  170.         // TODO: adjust the naming of the xml params to be consistent consistent, but this will be a bc break
  171.         $filters['structureTypes'] = \array_merge($filters['types'] ?? [], $this->getStructureTypesProperty($propertyParameter));
  172.         $filters['types'] = $this->getTypesProperty($propertyParameter);
  173.         $filters['excluded'] = $this->getExcludedFilter($filters$propertyParameter);
  174.         $locale $options['locale'];
  175.         $webspaceKey $this->getWebspaceKey($propertyParameter$options);
  176.         $queryResult $this->getSearchResult($filters$limit$page$pageSize$locale$webspaceKey);
  177.         $result = [];
  178.         /** @var ArticleViewDocumentInterface $document */
  179.         foreach ($queryResult as $document) {
  180.             $this->referenceStore->add($document->getUuid());
  181.             $result[] = $this->articleResourceItemFactory->createResourceItem($document);
  182.         }
  183.         return new DataProviderResult($result$this->hasNextPage($queryResult$limit$page$pageSize));
  184.     }
  185.     public function resolveDatasource($datasource, array $propertyParameter, array $options)
  186.     {
  187.         return;
  188.     }
  189.     private function getWebspaceKey(array $propertyParameter, array $options): ?string
  190.     {
  191.         if (\array_key_exists('ignoreWebspaces'$propertyParameter)) {
  192.             $value $propertyParameter['ignoreWebspaces']->getValue();
  193.             if (true === $value) {
  194.                 return null;
  195.             }
  196.         }
  197.         if (\array_key_exists('webspaceKey'$options)) {
  198.             return $options['webspaceKey'];
  199.         }
  200.         return null;
  201.     }
  202.     /**
  203.      * Returns flag "hasNextPage".
  204.      * It combines the limit/query-count with the page and page-size.
  205.      */
  206.     private function hasNextPage(\Countable $queryResult, ?int $limitint $page, ?int $pageSize): bool
  207.     {
  208.         $count $queryResult->count();
  209.         if (null === $pageSize || $pageSize $this->defaultLimit) {
  210.             $pageSize $this->defaultLimit;
  211.         }
  212.         $offset = ($page 1) * $pageSize;
  213.         if ($limit && $offset $pageSize $limit) {
  214.             return false;
  215.         }
  216.         return $count > ($page $pageSize);
  217.     }
  218.     /**
  219.      * Creates search for filters and returns search-result.
  220.      */
  221.     private function getSearchResult(array $filters, ?int $limitint $page, ?int $pageSize, ?string $locale, ?string $webspaceKey): \Countable
  222.     {
  223.         $repository $this->searchManager->getRepository($this->articleDocumentClass);
  224.         $search $this->createSearch($repository->createSearch(), $filters$locale);
  225.         if (!$search) {
  226.             return new \ArrayIterator([]);
  227.         }
  228.         $this->addPagination($search$pageSize$page$limit);
  229.         if (\array_key_exists('sortBy'$filters)) {
  230.             $sortMethod = \array_key_exists('sortMethod'$filters) ? $filters['sortMethod'] : 'asc';
  231.             $search->addSort(new FieldSort($filters['sortBy'], $sortMethod));
  232.         }
  233.         if ($webspaceKey) {
  234.             $webspaceQuery = new BoolQuery();
  235.             // check for mainWebspace
  236.             $webspaceQuery->add(new TermQuery('main_webspace'$webspaceKey), BoolQuery::SHOULD);
  237.             // check for additionalWebspaces
  238.             $webspaceQuery->add(new TermQuery('additional_webspaces'$webspaceKey), BoolQuery::SHOULD);
  239.             $search->addQuery($webspaceQuery);
  240.         }
  241.         $segmentKey $filters['segmentKey'] ?? null;
  242.         if ($segmentKey && $webspaceKey) {
  243.             $matchingSegmentQuery = new TermQuery(
  244.                 'excerpt.segments.assignment_key',
  245.                 $webspaceKey SegmentSelect::SEPARATOR $segmentKey
  246.             );
  247.             $noSegmentQuery = new BoolQuery();
  248.             $noSegmentQuery->add(new TermQuery('excerpt.segments.webspace_key'$webspaceKey), BoolQuery::MUST_NOT);
  249.             $segmentQuery = new BoolQuery();
  250.             $segmentQuery->add($matchingSegmentQueryBoolQuery::SHOULD);
  251.             $segmentQuery->add($noSegmentQueryBoolQuery::SHOULD);
  252.             $search->addQuery($segmentQuery);
  253.         }
  254.         return $repository->findDocuments($search);
  255.     }
  256.     /**
  257.      * Initialize search with neccesary queries.
  258.      */
  259.     protected function createSearch(Search $search, array $filtersstring $locale): Search
  260.     {
  261.         if (< \count($filters['excluded'])) {
  262.             foreach ($filters['excluded'] as $uuid) {
  263.                 $search->addQuery(new TermQuery('uuid'$uuid), BoolQuery::MUST_NOT);
  264.             }
  265.         }
  266.         $query = new BoolQuery();
  267.         $queriesCount 0;
  268.         $operator $this->getFilter($filters'tagOperator''or');
  269.         $this->addBoolQuery('tags'$filters'excerpt.tags.id'$operator$query$queriesCount);
  270.         $operator $this->getFilter($filters'websiteTagsOperator''or');
  271.         $this->addBoolQuery('websiteTags'$filters'excerpt.tags.id'$operator$query$queriesCount);
  272.         $operator $this->getFilter($filters'categoryOperator''or');
  273.         $this->addBoolQuery('categories'$filters'excerpt.categories.id'$operator$query$queriesCount);
  274.         $operator $this->getFilter($filters'websiteCategoriesOperator''or');
  275.         $this->addBoolQuery('websiteCategories'$filters'excerpt.categories.id'$operator$query$queriesCount);
  276.         if (null !== $locale) {
  277.             $search->addQuery(new TermQuery('locale'$locale));
  278.         }
  279.         if (\array_key_exists('types'$filters) && $filters['types']) {
  280.             $typesQuery = new BoolQuery();
  281.             foreach ($filters['types'] as $typeFilter) {
  282.                 $typesQuery->add(new TermQuery('type'$typeFilter), BoolQuery::SHOULD);
  283.             }
  284.             $search->addQuery($typesQuery);
  285.         }
  286.         if (\array_key_exists('structureTypes'$filters) && $filters['structureTypes']) {
  287.             $strTypesQuery = new BoolQuery();
  288.             foreach ($filters['structureTypes'] as $filter) {
  289.                 $strTypesQuery->add(new TermQuery('structure_type'$filter), BoolQuery::SHOULD);
  290.             }
  291.             $search->addQuery($strTypesQuery);
  292.         }
  293.         if (=== $queriesCount) {
  294.             $search->addQuery(new MatchAllQuery(), BoolQuery::MUST);
  295.         } else {
  296.             $search->addQuery($queryBoolQuery::MUST);
  297.         }
  298.         return $search;
  299.     }
  300.     /**
  301.      * Returns array with all types defined in property parameter.
  302.      */
  303.     private function getTypesProperty(array $propertyParameter): array
  304.     {
  305.         $filterTypes = [];
  306.         if (\array_key_exists('types'$propertyParameter)
  307.             && !empty($value $propertyParameter['types']->getValue())
  308.             && \is_string($value)) {
  309.             $types = \explode(','$value);
  310.             foreach ($types as $type) {
  311.                 $filterTypes[] = $type;
  312.             }
  313.         }
  314.         return $filterTypes;
  315.     }
  316.     /**
  317.      * Returns array with all structure types (template keys) defined in property parameter.
  318.      */
  319.     private function getStructureTypesProperty(array $propertyParameter): array
  320.     {
  321.         $filterStrTypes = [];
  322.         if (\array_key_exists('structureTypes'$propertyParameter)
  323.             && null !== ($types = \explode(','$propertyParameter['structureTypes']->getValue()))
  324.         ) {
  325.             foreach ($types as $type) {
  326.                 $filterStrTypes[] = $type;
  327.             }
  328.         }
  329.         return $filterStrTypes;
  330.     }
  331.     /**
  332.      * Returns excluded articles.
  333.      *
  334.      * @param PropertyParameter[] $propertyParameter
  335.      */
  336.     private function getExcludedFilter(array $filters, array $propertyParameter): array
  337.     {
  338.         $excluded = \array_key_exists('excluded'$filters) ? $filters['excluded'] : [];
  339.         if (\array_key_exists('exclude_duplicates'$propertyParameter)
  340.             && $propertyParameter['exclude_duplicates']->getValue()
  341.         ) {
  342.             $excluded = \array_merge($excluded$this->referenceStore->getAll());
  343.         }
  344.         return $excluded;
  345.     }
  346.     /**
  347.      * Add the pagination to given query.
  348.      */
  349.     private function addPagination(Search $search, ?int $pageSizeint $page, ?int $limit): void
  350.     {
  351.         if (null === $pageSize || $pageSize $this->defaultLimit) {
  352.             $pageSize $this->defaultLimit;
  353.         }
  354.         $offset = ($page 1) * $pageSize;
  355.         if ($limit && $offset $pageSize $limit) {
  356.             $pageSize $limit $offset;
  357.         }
  358.         if ($pageSize 0) {
  359.             $pageSize 0;
  360.         }
  361.         $search->setFrom($offset);
  362.         $search->setSize($pageSize);
  363.     }
  364.     /**
  365.      * Add a boolean-query if filter exists.
  366.      */
  367.     private function addBoolQuery(
  368.         string $filterName,
  369.         array $filters,
  370.         string $field,
  371.         string $operator,
  372.         BoolQuery $query,
  373.         int &$queriesCount
  374.     ): void {
  375.         if (!== \count($tags $this->getFilter($filters$filterName, []))) {
  376.             ++$queriesCount;
  377.             $query->add($this->getBoolQuery($field$tags$operator));
  378.         }
  379.     }
  380.     /**
  381.      * Returns boolean query for given fields and values.
  382.      */
  383.     private function getBoolQuery(string $field, array $valuesstring $operator): BoolQuery
  384.     {
  385.         $type = ('or' === \strtolower($operator) ? BoolQuery::SHOULD BoolQuery::MUST);
  386.         $query = new BoolQuery();
  387.         foreach ($values as $value) {
  388.             $query->add(new TermQuery($field$value), $type);
  389.         }
  390.         return $query;
  391.     }
  392.     /**
  393.      * Returns filter value.
  394.      *
  395.      * @param mixed $default
  396.      *
  397.      * @return mixed
  398.      */
  399.     private function getFilter(array $filtersstring $name$default null)
  400.     {
  401.         if ($this->hasFilter($filters$name)) {
  402.             return $filters[$name];
  403.         }
  404.         return $default;
  405.     }
  406.     /**
  407.      * @return array<int, array<string, string>>
  408.      */
  409.     private function getTypes(): array
  410.     {
  411.         $types = [];
  412.         if ($this->tokenStorage && null !== $this->tokenStorage->getToken() && $this->formMetadataProvider) {
  413.             $user $this->tokenStorage->getToken()->getUser();
  414.             if (!$user instanceof UserInterface) {
  415.                 return $types;
  416.             }
  417.             /** @var TypedFormMetadata $metadata */
  418.             $metadata $this->formMetadataProvider->getMetadata('article'$user->getLocale(), []);
  419.             foreach ($metadata->getForms() as $form) {
  420.                 $types[] = ['type' => $form->getName(), 'title' => $form->getTitle()];
  421.             }
  422.         }
  423.         return $types;
  424.     }
  425.     /**
  426.      * Returns true if filter-value exists.
  427.      */
  428.     private function hasFilter(array $filtersstring $name): bool
  429.     {
  430.         return \array_key_exists($name$filters) && null !== $filters[$name];
  431.     }
  432.     /**
  433.      * Returns Proxy document for uuid.
  434.      */
  435.     private function getResource(string $uuidstring $locale): object
  436.     {
  437.         return $this->proxyFactory->createProxy(
  438.             ArticleDocument::class,
  439.             function(
  440.                 &$wrappedObject,
  441.                 LazyLoadingInterface $proxy,
  442.                 $method,
  443.                 array $parameters,
  444.                 &$initializer
  445.             ) use ($uuid$locale) {
  446.                 $initializer null;
  447.                 $wrappedObject $this->documentManager->find($uuid$locale);
  448.                 return true;
  449.             }
  450.         );
  451.     }
  452.     public function getAlias()
  453.     {
  454.         return 'article';
  455.     }
  456. }