diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index dd2cf7a3..7aeba302 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -14,7 +14,6 @@ use Sulu\Bundle\ArticleBundle\Document\ArticlePageViewObject; use Sulu\Bundle\ArticleBundle\Document\ArticleViewDocument; use Sulu\Bundle\ArticleBundle\Domain\Model\Article; -use Sulu\Bundle\ArticleBundle\Domain\Model\ArticleDimensionContent; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; diff --git a/DependencyInjection/SuluArticleExtension.php b/DependencyInjection/SuluArticleExtension.php index 56282a70..fee6c318 100644 --- a/DependencyInjection/SuluArticleExtension.php +++ b/DependencyInjection/SuluArticleExtension.php @@ -328,7 +328,6 @@ public function load(array $configs, ContainerBuilder $container) $storage = $config['storage']; $container->setParameter('sulu_article.article_storage', $storage); - $container->setParameter('sulu_article.default_main_webspace', $config['default_main_webspace']); $container->setParameter('sulu_article.default_additional_webspaces', $config['default_additional_webspaces']); $container->setParameter('sulu_article.types', $config['types']); diff --git a/UPGRADE.md b/UPGRADE.md index 0465c7e5..088b5292 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -2,6 +2,16 @@ ## 3.x +### Article user settings updated + +Due to the refactoring of the new content bundle storage, the user settings for articles must be erased to avoid conflicts. + +Execute the following SQL query to delete the user settings: +```mysql +DELETE FROM `se_user_settings` + WHERE `se_user_settings`.`settingsKey` LIKE '%article%' +``` + ### Elasticsearch Bundle need to be required The SuluArticleBundle defines not longer its dependency to `handcraftedinthealps/elasticsearch-bundle` because diff --git a/config/lists/articles.xml b/config/lists/articles.xml index a2c26773..1595233f 100644 --- a/config/lists/articles.xml +++ b/config/lists/articles.xml @@ -2,6 +2,27 @@ articles + + + %sulu.model.user.class% + Sulu\Article\Domain\Model\ArticleInterface.creator + + + %sulu.model.contact.class% + %sulu.model.user.class%.contact + + + + + %sulu.model.user.class% + Sulu\Article\Domain\Model\ArticleInterface.changer + + + %sulu.model.contact.class% + %sulu.model.user.class%.contact + + + dimensionContent @@ -29,6 +50,13 @@ + + + authorEntity + dimensionContent.author + + + @@ -54,6 +82,71 @@ + + + firstName + authorEntity + + + + + lastName + authorEntity + + + + + + + + + + + + created + Sulu\Article\Domain\Model\ArticleInterface + + + + + changed + Sulu\Article\Domain\Model\ArticleInterface + + + + + + firstName + %sulu.model.contact.class% + + + + + lastName + %sulu.model.contact.class% + + + + + + + + firstName + %sulu.model.contact.class% + + + + + lastName + %sulu.model.contact.class% + + + + + locale dimensionContent diff --git a/src/Domain/Exception/ArticleNotFoundException.php b/src/Domain/Exception/ArticleNotFoundException.php index 000b59bf..1b7ccd91 100644 --- a/src/Domain/Exception/ArticleNotFoundException.php +++ b/src/Domain/Exception/ArticleNotFoundException.php @@ -28,7 +28,7 @@ class ArticleNotFoundException extends \Exception /** * @param array $filters */ - public function __construct(array $filters, int $code = 0, \Throwable $previous = null) + public function __construct(array $filters, int $code = 0, ?\Throwable $previous = null) { $this->model = ArticleInterface::class; diff --git a/src/Domain/Repository/ArticleRepositoryInterface.php b/src/Domain/Repository/ArticleRepositoryInterface.php index d446dd0f..3a8e445f 100644 --- a/src/Domain/Repository/ArticleRepositoryInterface.php +++ b/src/Domain/Repository/ArticleRepositoryInterface.php @@ -100,6 +100,31 @@ public function findOneBy(array $filters, array $selects = []): ?ArticleInterfac */ public function findBy(array $filters = [], array $sortBy = [], array $selects = []): iterable; + /** + * @param array{ + * uuid?: string, + * uuids?: string[], + * locale?: string, + * stage?: string, + * categoryIds?: int[], + * categoryKeys?: string[], + * categoryOperator?: 'AND'|'OR', + * tagIds?: int[], + * tagNames?: string[], + * tagOperator?: 'AND'|'OR', + * templateKeys?: string[], + * page?: int, + * limit?: int, + * } $filters + * @param array{ + * id?: 'asc'|'desc', + * title?: 'asc'|'desc', + * } $sortBy + * + * @return iterable + */ + public function findIdentifiersBy(array $filters = [], array $sortBy = []): iterable; + /** * @param array{ * uuid?: string, diff --git a/src/Infrastructure/Doctrine/Repository/ArticleRepository.php b/src/Infrastructure/Doctrine/Repository/ArticleRepository.php index d3eda575..3712b5d5 100644 --- a/src/Infrastructure/Doctrine/Repository/ArticleRepository.php +++ b/src/Infrastructure/Doctrine/Repository/ArticleRepository.php @@ -14,6 +14,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\NoResultException; +use Doctrine\ORM\Query\Expr\OrderBy; use Doctrine\ORM\QueryBuilder; use Sulu\Article\Domain\Exception\ArticleNotFoundException; use Sulu\Article\Domain\Model\ArticleDimensionContentInterface; @@ -149,6 +150,24 @@ public function findBy(array $filters = [], array $sortBy = [], array $selects = } } + public function findIdentifiersBy(array $filters = [], array $sortBy = []): iterable + { + $queryBuilder = $this->createQueryBuilder($filters, $sortBy); + + $queryBuilder->select('DISTINCT article.uuid'); + + // we need to select the fields which are used in the order by clause + /** @var OrderBy $orderBy */ + foreach ($queryBuilder->getDQLPart('orderBy') as $orderBy) { + $queryBuilder->addSelect(\explode(' ', $orderBy->getParts()[0])[0]); + } + + /** @var iterable $identifiers */ + $identifiers = $queryBuilder->getQuery()->getResult(); + + return $identifiers; + } + public function add(ArticleInterface $article): void { $this->entityManager->persist($article); @@ -228,17 +247,29 @@ private function createQueryBuilder(array $filters, array $sortBy = [], array $s $queryBuilder->setFirstResult($offset); } - if (\array_key_exists('locale', $filters) // should also work with locale = null - && \array_key_exists('stage', $filters)) { + if ( + (\array_key_exists('locale', $filters) // should also work with locale = null + && \array_key_exists('stage', $filters)) + || ([] === $filters && [] !== $sortBy) // if no filters are set, but sortBy is set, we need to set the sorting + ) { $this->dimensionContentQueryEnhancer->addFilters( $queryBuilder, 'article', $this->articleDimensionContentClassName, - $filters + $filters, + $sortBy ); } - // TODO add sortBys + if ([] !== $sortBy) { + foreach ($sortBy as $field => $order) { + if ('uuid' === $field) { + $queryBuilder->addOrderBy('article.uuid', $order); + } elseif ('created' === $field) { + $queryBuilder->addOrderBy('article.created', $order); + } + } + } // selects if ($selects[self::SELECT_ARTICLE_CONTENT] ?? null) { diff --git a/src/Infrastructure/Sulu/Content/ArticleDataProvider.php b/src/Infrastructure/Sulu/Content/ArticleDataProvider.php new file mode 100644 index 00000000..3eb2ca12 --- /dev/null +++ b/src/Infrastructure/Sulu/Content/ArticleDataProvider.php @@ -0,0 +1,176 @@ +getConfigurationBuilder()->getConfiguration(); + } + + /** + * Create new configuration-builder. + */ + protected function getConfigurationBuilder(): BuilderInterface + { + $builder = Builder::create() + ->enableTags() + ->enableCategories() + ->enableLimit() + ->enablePagination() + ->enablePresentAs() + ->enableSorting( + [ + ['column' => 'workflowPublished', 'title' => 'sulu_admin.published'], + ['column' => 'authored', 'title' => 'sulu_admin.authored'], + ['column' => 'created', 'title' => 'sulu_admin.created'], + ['column' => 'title', 'title' => 'sulu_admin.title'], + ['column' => 'author', 'title' => 'sulu_admin.author'], + ] + ); + + return $builder; + } + + public function getDefaultPropertyParameter(): array + { + return [ + 'type' => new PropertyParameter('type', null), + 'ignoreWebspaces' => new PropertyParameter('ignoreWebspaces', false), + ]; + } + + public function resolveDataItems(array $filters, array $propertyParameter, array $options = [], $limit = null, $page = 1, $pageSize = null) + { + [$filters, $sortBy] = $this->resolveFilters($filters, $page, $options['locale']); + + $dimensionAttributes = [ + 'locale' => $options['locale'], + 'stage' => $this->showDrafts ? DimensionContentInterface::STAGE_DRAFT : DimensionContentInterface::STAGE_LIVE, + ]; + + $identifiers = $this->articleRepository->findIdentifiersBy( + filters: \array_merge($dimensionAttributes, $filters), + sortBy: $sortBy + ); + + $articles = $this->articleRepository->findBy( + filters: \array_merge($dimensionAttributes, ['uuids' => $identifiers]), + sortBy: $sortBy, + selects: [ArticleRepositoryInterface::GROUP_SELECT_ARTICLE_ADMIN => true] + ); + + $result = []; + foreach ($articles as $article) { + $dimensionContent = $this->contentManager->resolve($article, $dimensionAttributes); + $result[] = [ + 'id' => $article->getId(), + 'title' => $dimensionContent->getTitle(), + ]; + } + $hasNextPage = \count($result) > ($pageSize ?? $limit); + + return new DataProviderResult($result, $hasNextPage); + } + + public function resolveResourceItems(array $filters, array $propertyParameter, array $options = [], $limit = null, $page = 1, $pageSize = null): DataProviderResult + { + [$filters, $sortBy] = $this->resolveFilters($filters, $page, $options['locale']); + + $dimensionAttributes = [ + 'locale' => $options['locale'], + 'stage' => $this->showDrafts ? DimensionContentInterface::STAGE_DRAFT : DimensionContentInterface::STAGE_LIVE, + ]; + + $identifiers = $this->articleRepository->findIdentifiersBy( + filters: \array_merge($dimensionAttributes, $filters), + sortBy: $sortBy + ); + + $articles = $this->articleRepository->findBy( + filters: \array_merge($dimensionAttributes, ['uuids' => $identifiers]), + sortBy: $sortBy, + selects: [ArticleRepositoryInterface::GROUP_SELECT_ARTICLE_WEBSITE => true] + ); + + $result = []; + foreach ($articles as $article) { + $dimensionContent = $this->contentManager->resolve($article, $dimensionAttributes); + $result[] = $this->contentManager->normalize($dimensionContent); + $this->articleReferenceStore->add($article->getId()); + } + $hasNextPage = \count($result) > ($pageSize ?? $limit); + + return new DataProviderResult($result, $hasNextPage); + } + + protected function resolveFilters(array $filters, int $page, string $locale): array + { + $filter = [ + 'locale' => $locale, + ]; + $sortBy = []; + if (isset($filters['categories'])) { + $filter['categoryIds'] = $filters['categories']; + } + if (isset($filters['categoryOperator'])) { + $filter['categoryOperator'] = $filters['categoryOperator']; + } + if (isset($filters['tags'])) { + $filter['tagIds'] = $filters['tags']; + } + if (isset($filters['tagOperator'])) { + $filter['tagOperator'] = $filters['tagOperator']; + } + if (isset($filters['limitResult'])) { + $filter['limit'] = (int) $filters['limitResult']; + } + $filter['page'] = $page; + + if (isset($filters['sortBy']) && isset($filters['sortMethod'])) { + $sortBy[$filters['sortBy']] = $filters['sortMethod']; + } + + return [$filter, $sortBy]; + } + + public function resolveDatasource($datasource, array $propertyParameter, array $options): void + { + return; + } + + public function getAlias() + { + return 'article'; + } +} diff --git a/src/Infrastructure/Sulu/Content/ArticleSelectionContentType.php b/src/Infrastructure/Sulu/Content/ArticleSelectionContentType.php index 04463ee8..e0eff423 100644 --- a/src/Infrastructure/Sulu/Content/ArticleSelectionContentType.php +++ b/src/Infrastructure/Sulu/Content/ArticleSelectionContentType.php @@ -1,9 +1,17 @@ referenceStore = $referenceStore; $this->contentManager = $contentManager; @@ -50,7 +57,6 @@ public function getContentData(PropertyInterface $property) ArticleRepositoryInterface::GROUP_SELECT_ARTICLE_WEBSITE => true, ]); - $result = []; foreach ($article as $article) { $dimensionContent = $this->contentManager->resolve($article, $dimensionAttributes); @@ -60,7 +66,6 @@ public function getContentData(PropertyInterface $property) \ksort($result); return \array_values($result); - } public function preResolve(PropertyInterface $property) diff --git a/src/Infrastructure/Sulu/Content/ArticleSitemapProvider.php b/src/Infrastructure/Sulu/Content/ArticleSitemapProvider.php index fa489fd2..23d92310 100644 --- a/src/Infrastructure/Sulu/Content/ArticleSitemapProvider.php +++ b/src/Infrastructure/Sulu/Content/ArticleSitemapProvider.php @@ -1,5 +1,14 @@ articleRepository = $articleRepository; @@ -54,8 +62,8 @@ public function getContentData(PropertyInterface $property) ArticleRepositoryInterface::GROUP_SELECT_ARTICLE_WEBSITE => true, ]); - $dimensionContent = $this->contentManager->resolve($article, $dimensionAttributes); + return $this->contentManager->normalize($dimensionContent); } @@ -69,4 +77,3 @@ public function preResolve(PropertyInterface $property) $this->referenceStore->add($uuid); } } - diff --git a/src/Infrastructure/Symfony/HttpKernel/SuluArticleBundle.php b/src/Infrastructure/Symfony/HttpKernel/SuluArticleBundle.php index 34a8886f..abf1bc5a 100644 --- a/src/Infrastructure/Symfony/HttpKernel/SuluArticleBundle.php +++ b/src/Infrastructure/Symfony/HttpKernel/SuluArticleBundle.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of Sulu. + * + * (c) Sulu GmbH + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + namespace Sulu\Article\Infrastructure\Symfony\HttpKernel; use Sulu\Article\Application\Mapper\ArticleContentMapper; @@ -18,6 +27,7 @@ use Sulu\Article\Domain\Repository\ArticleRepositoryInterface; use Sulu\Article\Infrastructure\Doctrine\Repository\ArticleRepository; use Sulu\Article\Infrastructure\Sulu\Admin\ArticleAdmin; +use Sulu\Article\Infrastructure\Sulu\Content\ArticleDataProvider; use Sulu\Article\Infrastructure\Sulu\Content\ArticleLinkProvider; use Sulu\Article\Infrastructure\Sulu\Content\ArticleSelectionContentType; use Sulu\Article\Infrastructure\Sulu\Content\ArticleSitemapProvider; @@ -26,8 +36,6 @@ use Sulu\Article\UserInterface\Controller\Admin\ArticleController; use Sulu\Bundle\ContentBundle\Content\Infrastructure\Sulu\Preview\ContentObjectProvider; use Sulu\Bundle\ContentBundle\Content\Infrastructure\Sulu\Search\ContentSearchMetadataProvider; -use Sulu\Bundle\ContentBundle\Content\Infrastructure\Sulu\SmartContent\Provider\ContentDataProvider; -use Sulu\Bundle\ContentBundle\Content\Infrastructure\Sulu\SmartContent\Repository\ContentDataProviderRepository; use Sulu\Bundle\PersistenceBundle\DependencyInjection\PersistenceExtensionTrait; use Sulu\Bundle\PersistenceBundle\PersistenceBundleTrait; use Sulu\Bundle\WebsiteBundle\ReferenceStore\ReferenceStore; @@ -224,17 +232,7 @@ public function loadExtension(array $config, ContainerConfigurator $container, C new Reference('sulu_page.structure.factory'), new Reference('doctrine.orm.entity_manager'), ]) - ->tag('sulu.link.provider', ['alias' => ArticleInterface::RESOURCE_KEY]); - - // Smart Content services - $services->set('sulu_article.article_data_provider_repository') - ->class(ContentDataProviderRepository::class) // TODO this should not be handled via Content Bundle instead own service which uses the ArticleRepository - ->args([ - new Reference('sulu_content.content_manager'), - new Reference('doctrine.orm.entity_manager'), - '%sulu_document_manager.show_drafts%', - ArticleInterface::class, - ]); + ->tag('sulu.link.provider', ['alias' => 'article']); $services->set('sulu_article.article_reference_store') ->class(ReferenceStore::class) @@ -245,7 +243,7 @@ public function loadExtension(array $config, ContainerConfigurator $container, C ->args([ new Reference('sulu_article.article_repository'), new Reference('sulu_content.content_manager'), - new Reference('sulu_article.article_reference_store') + new Reference('sulu_article.article_reference_store'), ]) ->tag('sulu.content.type', ['alias' => 'single_article_selection']); @@ -254,17 +252,18 @@ public function loadExtension(array $config, ContainerConfigurator $container, C ->args([ new Reference('sulu_article.article_repository'), new Reference('sulu_content.content_manager'), - new Reference('sulu_article.article_reference_store') + new Reference('sulu_article.article_reference_store'), ]) ->tag('sulu.content.type', ['alias' => 'article_selection']); + // Smart Content services $services->set('sulu_article.article_data_provider') - ->class(ContentDataProvider::class) // TODO this should not be handled via Content Bundle instead own service which uses the ArticleRepository + ->class(ArticleDataProvider::class) // TODO this should not be handled via Content Bundle instead own service which uses the ArticleRepository ->args([ - new Reference('sulu_article.article_data_provider_repository'), - new Reference('sulu_core.array_serializer'), + new Reference('sulu_article.article_repository'), new Reference('sulu_content.content_manager'), new Reference('sulu_article.article_reference_store'), + '%sulu_document_manager.show_drafts%', ]) ->tag('sulu.smart_content.data_provider', ['alias' => ArticleInterface::RESOURCE_KEY]); diff --git a/src/UserInterface/Controller/Admin/ArticleController.php b/src/UserInterface/Controller/Admin/ArticleController.php index c36f24ce..a56847c9 100644 --- a/src/UserInterface/Controller/Admin/ArticleController.php +++ b/src/UserInterface/Controller/Admin/ArticleController.php @@ -231,11 +231,13 @@ private function handleAction(Request $request, string $uuid): ?ArticleInterface (string) $request->query->get('src'), (string) $request->query->get('dest') ); + /** @see Sulu\Article\Application\MessageHandler\CopyLocaleArticleMessageHandler */ /** @var null */ return $this->handle(new Envelope($message, [new EnableFlushStamp()])); } else { $message = new ApplyWorkflowTransitionArticleMessage(['uuid' => $uuid], $this->getLocale($request), $action); + /** @see Sulu\Article\Application\MessageHandler\ApplyWorkflowTransitionArticleMessageHandler */ /** @var null */ return $this->handle(new Envelope($message, [new EnableFlushStamp()]));