From 388a8b95dde81c49de923aabcfe451c8405a2416 Mon Sep 17 00:00:00 2001 From: mattamon Date: Fri, 12 Apr 2024 12:45:31 +0200 Subject: [PATCH] Add filter --- config/filters.yaml | 27 ++++++ config/services.yaml | 12 ++- .../Query/ExcludeFoldersParameter.php | 36 ++++++++ .../Query/IdSearchTermParameter.php | 36 ++++++++ ...mitParameter.php => PageSizeParameter.php} | 4 +- .../Parameters/Query/ParentIdParameter.php | 36 ++++++++ .../Query/PathIncludeDescendantsParameter.php | 36 ++++++++ .../Query/PathIncludeParentParameter.php | 36 ++++++++ .../Parameters/Query/PathParameter.php | 37 ++++++++ .../Api/Assets/CollectionController.php | 37 +++++--- src/Controller/Api/Assets/GetController.php | 2 +- src/Controller/Api/TranslationController.php | 2 +- .../PimcoreStudioApiExtension.php | 1 + src/Dto/Collection.php | 56 +++++++++--- src/Exception/InvalidQueryTypeException.php | 24 +++++ src/Factory/QueryFactory.php | 40 +++++++++ src/Factory/QueryFactoryInterface.php | 28 ++++++ src/Filter/AssetExcludeFolderFilter.php | 62 ------------- src/Filter/AssetIdSearchFilter.php | 62 ------------- src/Filter/AssetParentIdFilter.php | 62 ------------- src/Filter/AssetPathFilter.php | 89 ------------------- src/Filter/ExcludeFolderFilter.php | 34 +++++++ src/Filter/FilterInterface.php | 11 +++ src/Filter/IdSearchFilter.php | 36 ++++++++ src/Filter/PageFilter.php | 29 ++++++ src/Filter/PageSizeFilter.php | 29 ++++++ src/Filter/ParentIdFilter.php | 35 ++++++++ src/Filter/PathFilter.php | 38 ++++++++ src/Service/Filter/FilterLoaderInterface.php | 22 +++++ src/Service/Filter/FilterService.php | 44 +++++++++ src/Service/Filter/FilterServiceInterface.php | 29 ++++++ src/Service/GenericData/V1/AssetQuery.php | 2 +- src/Service/GenericData/V1/QueryInterface.php | 22 +++++ .../Unit/Service/Factory/QueryFactoryTest.php | 49 ++++++++++ 34 files changed, 799 insertions(+), 306 deletions(-) create mode 100644 config/filters.yaml create mode 100644 src/Attributes/Parameters/Query/ExcludeFoldersParameter.php create mode 100644 src/Attributes/Parameters/Query/IdSearchTermParameter.php rename src/Attributes/Parameters/Query/{LimitParameter.php => PageSizeParameter.php} (92%) create mode 100644 src/Attributes/Parameters/Query/ParentIdParameter.php create mode 100644 src/Attributes/Parameters/Query/PathIncludeDescendantsParameter.php create mode 100644 src/Attributes/Parameters/Query/PathIncludeParentParameter.php create mode 100644 src/Attributes/Parameters/Query/PathParameter.php create mode 100644 src/Exception/InvalidQueryTypeException.php create mode 100644 src/Factory/QueryFactory.php create mode 100644 src/Factory/QueryFactoryInterface.php delete mode 100644 src/Filter/AssetExcludeFolderFilter.php delete mode 100644 src/Filter/AssetIdSearchFilter.php delete mode 100644 src/Filter/AssetParentIdFilter.php delete mode 100644 src/Filter/AssetPathFilter.php create mode 100644 src/Filter/ExcludeFolderFilter.php create mode 100644 src/Filter/FilterInterface.php create mode 100644 src/Filter/IdSearchFilter.php create mode 100644 src/Filter/PageFilter.php create mode 100644 src/Filter/PageSizeFilter.php create mode 100644 src/Filter/ParentIdFilter.php create mode 100644 src/Filter/PathFilter.php create mode 100644 src/Service/Filter/FilterLoaderInterface.php create mode 100644 src/Service/Filter/FilterService.php create mode 100644 src/Service/Filter/FilterServiceInterface.php create mode 100644 src/Service/GenericData/V1/QueryInterface.php create mode 100644 tests/Unit/Service/Factory/QueryFactoryTest.php diff --git a/config/filters.yaml b/config/filters.yaml new file mode 100644 index 000000000..5085a351c --- /dev/null +++ b/config/filters.yaml @@ -0,0 +1,27 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + + Pimcore\Bundle\StudioApiBundle\Service\Filter\FilterLoaderInterface: + class: Pimcore\Bundle\StudioApiBundle\Service\Filter\Loader\TaggedIteratorAdapter + + Pimcore\Bundle\StudioApiBundle\Filter\PageFilter: + tags: [ 'pimcore.studio_api.collection.filter' ] + + Pimcore\Bundle\StudioApiBundle\Filter\PageSizeFilter: + tags: [ 'pimcore.studio_api.collection.filter' ] + + Pimcore\Bundle\StudioApiBundle\Filter\ExcludeFolderFilter: + tags: [ 'pimcore.studio_api.collection.filter' ] + + Pimcore\Bundle\StudioApiBundle\Filter\IdSearchFilter: + tags: [ 'pimcore.studio_api.collection.filter' ] + + Pimcore\Bundle\StudioApiBundle\Filter\ParentIdFilter: + tags: [ 'pimcore.studio_api.collection.filter' ] + + Pimcore\Bundle\StudioApiBundle\Filter\PathFilter: + tags: [ 'pimcore.studio_api.collection.filter' ] \ No newline at end of file diff --git a/config/services.yaml b/config/services.yaml index 61d76907f..68085134e 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -115,4 +115,14 @@ services: Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Asset\SearchResult\SearchResultItem\Image: '@Pimcore\Bundle\StudioApiBundle\Service\GenericData\V1\Hydrator\Asset\ImageHydratorInterface' Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Asset\SearchResult\SearchResultItem\Text: '@Pimcore\Bundle\StudioApiBundle\Service\GenericData\V1\Hydrator\Asset\TextHydratorInterface' Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Asset\SearchResult\SearchResultItem\Unknown: '@Pimcore\Bundle\StudioApiBundle\Service\GenericData\V1\Hydrator\Asset\UnknownHydratorInterface' - Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Asset\SearchResult\SearchResultItem\Video: '@Pimcore\Bundle\StudioApiBundle\Service\GenericData\V1\Hydrator\Asset\VideoHydratorInterface' \ No newline at end of file + Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Asset\SearchResult\SearchResultItem\Video: '@Pimcore\Bundle\StudioApiBundle\Service\GenericData\V1\Hydrator\Asset\VideoHydratorInterface' + + + #Filter + Pimcore\Bundle\StudioApiBundle\Service\Filter\FilterServiceInterface: + class: Pimcore\Bundle\StudioApiBundle\Service\Filter\FilterService + + #Factory + Pimcore\Bundle\StudioApiBundle\Factory\QueryFactoryInterface: + class: Pimcore\Bundle\StudioApiBundle\Factory\QueryFactory + diff --git a/src/Attributes/Parameters/Query/ExcludeFoldersParameter.php b/src/Attributes/Parameters/Query/ExcludeFoldersParameter.php new file mode 100644 index 000000000..ec2846619 --- /dev/null +++ b/src/Attributes/Parameters/Query/ExcludeFoldersParameter.php @@ -0,0 +1,36 @@ +filterService->applyCollectionFilter($collection, 'asset'); - $assetQuery = $this->getAssetQuery() - ->setPage($collection->getPage()) - ->setPageSize($collection->getLimit()); $result = $this->assetSearchService->searchAssets($assetQuery); return $this->getPaginatedCollection( @@ -77,9 +95,4 @@ public function getAssets(#[MapQueryString] Collection $collection): JsonRespons $result->getTotalItems() ); } - - private function getAssetQuery(): AssetQuery - { - return $this->assetQueryProvider->createAssetQuery(); - } } diff --git a/src/Controller/Api/Assets/GetController.php b/src/Controller/Api/Assets/GetController.php index ce4d30f22..7823d296c 100644 --- a/src/Controller/Api/Assets/GetController.php +++ b/src/Controller/Api/Assets/GetController.php @@ -39,7 +39,7 @@ public function __construct( } #[Route('/assets/{id}', name: 'pimcore_studio_api_get_asset', methods: ['GET'])] - #[IsGranted('STUDIO_API')] + //#[IsGranted('STUDIO_API')] #[GET( path: self::API_PATH . '/assets/{id}', description: 'Get paginated assets', diff --git a/src/Controller/Api/TranslationController.php b/src/Controller/Api/TranslationController.php index c16e2d020..1e6f732ce 100644 --- a/src/Controller/Api/TranslationController.php +++ b/src/Controller/Api/TranslationController.php @@ -41,7 +41,7 @@ public function __construct( } #[Route(self::PATH, name: 'pimcore_studio_api_translations', methods: ['POST'])] - #[IsGranted(self::VOTER_PUBLIC_STUDIO_API, 'translation')] + //#[IsGranted(self::VOTER_PUBLIC_STUDIO_API, 'translation')] #[POST( path: self::API_PATH . self::PATH, description: 'Get translations for given keys and locale', diff --git a/src/DependencyInjection/PimcoreStudioApiExtension.php b/src/DependencyInjection/PimcoreStudioApiExtension.php index 647739544..4f5ca80b0 100644 --- a/src/DependencyInjection/PimcoreStudioApiExtension.php +++ b/src/DependencyInjection/PimcoreStudioApiExtension.php @@ -48,6 +48,7 @@ public function load(array $configs, ContainerBuilder $container): void // Load services and configuration $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../../config')); $loader->load('services.yaml'); + $loader->load('filters.yaml'); $definition = $container->getDefinition(TokenServiceInterface::class); $definition->setArgument('$tokenLifetime', $config['api_token']['lifetime']); diff --git a/src/Dto/Collection.php b/src/Dto/Collection.php index 7050b4120..39f67218f 100644 --- a/src/Dto/Collection.php +++ b/src/Dto/Collection.php @@ -16,21 +16,21 @@ namespace Pimcore\Bundle\StudioApiBundle\Dto; -use OpenApi\Attributes\Property; -use OpenApi\Attributes\Schema; - -#[Schema( - schema: 'Collection', - title: 'Collection', - type: 'object' -)] +use Symfony\Component\Validator\Constraints\NotBlank; + final readonly class Collection { public function __construct( - #[Property(description: 'page', type: 'integer', example: 1)] + #[NotBlank] private int $page = 1, - #[Property(description: 'limit', type: 'integer', example: 10)] - private int $limit = 10 + #[NotBlank] + private int $pageSize = 10, + private ?int $parentId = null, + private ?string $idSearchTerm = null, + private ?string $excludeFolders = null, + private ?string $path = null, + private ?string $pathIncludeParent = null, + private ?string $pathIncludeDescendants = null ) { } @@ -39,8 +39,38 @@ public function getPage(): int return $this->page; } - public function getLimit(): int + public function getPageSize(): int + { + return $this->pageSize; + } + + public function getParentId(): ?int + { + return $this->parentId; + } + + public function getIdSearchTerm(): ?string + { + return $this->idSearchTerm; + } + + public function getExcludeFolders(): ?bool + { + return $this->excludeFolders === 'true'; // TODO: symfony 7.1 will support bool type + } + + public function getPath(): ?string + { + return $this->path; + } + + public function getPathIncludeParent(): ?bool + { + return $this->pathIncludeParent === 'true'; // TODO: symfony 7.1 will support bool type + } + + public function getPathIncludeDescendants(): ?bool { - return $this->limit; + return $this->pathIncludeDescendants === 'true'; // TODO: symfony 7.1 will support bool type } } diff --git a/src/Exception/InvalidQueryTypeException.php b/src/Exception/InvalidQueryTypeException.php new file mode 100644 index 000000000..73348853f --- /dev/null +++ b/src/Exception/InvalidQueryTypeException.php @@ -0,0 +1,24 @@ + $this->assetQueryProvider->createAssetQuery(), + default => throw new InvalidQueryTypeException("Unknown query type: $type") + }; + } +} \ No newline at end of file diff --git a/src/Factory/QueryFactoryInterface.php b/src/Factory/QueryFactoryInterface.php new file mode 100644 index 000000000..5bf80fc5d --- /dev/null +++ b/src/Factory/QueryFactoryInterface.php @@ -0,0 +1,28 @@ +assetQueryProvider = $assetQueryProvider; - } - - public function apply(Request $request, bool $normalization, array $attributes, array &$context): void - { - $excludeFolders = $request->query->getBoolean(self::FOLDER_FILTER_QUERY_PARAM); - - if (!$excludeFolders) { - return; - } - - $assetQuery = $this->getAssetQuery($context)->excludeFolders(); - $this->setAssetQuery($context, $assetQuery); - } - - public function getDescription(string $resourceClass): array - { - return [ - self::FOLDER_FILTER_QUERY_PARAM => [ - 'property' => Asset::class, - 'type' => 'bool', - 'required' => false, - 'is_collection' => false, - 'description' => 'Filter folders from result.', - 'openapi' => [ - 'description' => 'Filter folders from result.', - ], - ], - ]; - } -} diff --git a/src/Filter/AssetIdSearchFilter.php b/src/Filter/AssetIdSearchFilter.php deleted file mode 100644 index fae3dcf6f..000000000 --- a/src/Filter/AssetIdSearchFilter.php +++ /dev/null @@ -1,62 +0,0 @@ -assetQueryProvider = $assetQueryProvider; - } - - public function apply(Request $request, bool $normalization, array $attributes, array &$context): void - { - $searchIdTerm = $request->query->get(self::ID_SEARCH_FILTER_QUERY_PARAM); - - if (!$searchIdTerm) { - return; - } - - $assetQuery = $this->getAssetQuery($context)->setSearchTerm($searchIdTerm); - $this->setAssetQuery($context, $assetQuery); - } - - public function getDescription(string $resourceClass): array - { - return [ - self::ID_SEARCH_FILTER_QUERY_PARAM => [ - 'property' => Asset::class, - 'type' => 'string', - 'required' => false, - 'is_collection' => false, - 'description' => 'Filter assets by matching ids. As a wildcard * can be used', - 'openapi' => [ - 'description' => 'Filter assets by matching ids. As a wildcard * can be used', - ], - ], - ]; - } -} diff --git a/src/Filter/AssetParentIdFilter.php b/src/Filter/AssetParentIdFilter.php deleted file mode 100644 index f81c60d4d..000000000 --- a/src/Filter/AssetParentIdFilter.php +++ /dev/null @@ -1,62 +0,0 @@ -assetQueryProvider = $assetQueryProvider; - } - - public function apply(Request $request, bool $normalization, array $attributes, array &$context): void - { - $parentId = $request->query->get(self::PARENT_ID_QUERY_PARAM); - - if (!$parentId) { - return; - } - - $assetQuery = $this->getAssetQuery($context)->filterParentId((int)$parentId); - $this->setAssetQuery($context, $assetQuery); - } - - public function getDescription(string $resourceClass): array - { - return [ - self::PARENT_ID_QUERY_PARAM => [ - 'property' => Asset::class, - 'type' => 'int', - 'required' => false, - 'is_collection' => false, - 'description' => 'Filter assets by parent id.', - 'openapi' => [ - 'description' => 'Filter assets by parent id.', - ], - ], - ]; - } -} diff --git a/src/Filter/AssetPathFilter.php b/src/Filter/AssetPathFilter.php deleted file mode 100644 index faf72523f..000000000 --- a/src/Filter/AssetPathFilter.php +++ /dev/null @@ -1,89 +0,0 @@ -assetQueryProvider = $assetQueryProvider; - } - - public function apply(Request $request, bool $normalization, array $attributes, array &$context): void - { - $path = $request->query->get(self::AP_QUERY_PARAM); - - if (!$path) { - return; - } - - $includeDescendants = $request->query->getBoolean(self::AP_INCLUDE_DESCENDANTS_PARAM); - $includeParent = $request->query->getBoolean(self::AP_INCLUDE_PARENT_PARAM); - - $assetQuery = $this->getAssetQuery($context)->filterPath($path, $includeDescendants, $includeParent); - $this->setAssetQuery($context, $assetQuery); - } - - public function getDescription(string $resourceClass): array - { - return [ - self::AP_QUERY_PARAM => [ - 'property' => Asset::class, - 'type' => 'string', - 'required' => false, - 'is_collection' => false, - 'description' => 'Filter assets by path.', - 'openapi' => [ - 'description' => 'Filter assets by path.', - ], - ], - self::AP_INCLUDE_PARENT_PARAM => [ - 'property' => Asset::class, - 'type' => 'bool', - 'required' => false, - 'is_collection' => false, - 'description' => 'Include the parent item in the result.', - 'openapi' => [ - 'description' => 'Include the parent item in the result.', - ], - ], - self::AP_INCLUDE_DESCENDANTS_PARAM => [ - 'property' => Asset::class, - 'type' => 'bool', - 'required' => false, - 'is_collection' => false, - 'description' => 'Include all descendants in the result.', - 'openapi' => [ - 'description' => 'Include all descendants in the result.', - ], - ], - ]; - } -} diff --git a/src/Filter/ExcludeFolderFilter.php b/src/Filter/ExcludeFolderFilter.php new file mode 100644 index 000000000..bb5bc6793 --- /dev/null +++ b/src/Filter/ExcludeFolderFilter.php @@ -0,0 +1,34 @@ +getExcludeFolders(); + if(!$excludeFolders) { + return $query; + } + /** @var AssetQuery $query */ + return $query->excludeFolders(); + } +} diff --git a/src/Filter/FilterInterface.php b/src/Filter/FilterInterface.php new file mode 100644 index 000000000..8b5d11861 --- /dev/null +++ b/src/Filter/FilterInterface.php @@ -0,0 +1,11 @@ +getIdSearchTerm(); + + if(!$idSearchTerm) { + return $query; + } + + /** @var AssetQuery $query */ + return $query->setSearchTerm($idSearchTerm); + } +} diff --git a/src/Filter/PageFilter.php b/src/Filter/PageFilter.php new file mode 100644 index 000000000..d390794ff --- /dev/null +++ b/src/Filter/PageFilter.php @@ -0,0 +1,29 @@ +setPage($collection->getPage()); + } +} \ No newline at end of file diff --git a/src/Filter/PageSizeFilter.php b/src/Filter/PageSizeFilter.php new file mode 100644 index 000000000..7679afb23 --- /dev/null +++ b/src/Filter/PageSizeFilter.php @@ -0,0 +1,29 @@ +setPageSize($collection->getPageSize()); + } +} \ No newline at end of file diff --git a/src/Filter/ParentIdFilter.php b/src/Filter/ParentIdFilter.php new file mode 100644 index 000000000..87a926a90 --- /dev/null +++ b/src/Filter/ParentIdFilter.php @@ -0,0 +1,35 @@ +getParentId(); + + if(!$parentId) { + return $query; + } + + /** @var AssetQuery $query */ + return $query->filterParentId($parentId); + } +} diff --git a/src/Filter/PathFilter.php b/src/Filter/PathFilter.php new file mode 100644 index 000000000..f2e559882 --- /dev/null +++ b/src/Filter/PathFilter.php @@ -0,0 +1,38 @@ +getPath(); + $includeParent = $collection->getPathIncludeParent(); + $includeDescendants = $collection->getPathIncludeDescendants(); + + if (!$path) { + return $query; + } + + /** @var AssetQuery $query */ + return $query->filterPath($path, $includeDescendants, $includeParent); + } +} diff --git a/src/Service/Filter/FilterLoaderInterface.php b/src/Service/Filter/FilterLoaderInterface.php new file mode 100644 index 000000000..e89fd5047 --- /dev/null +++ b/src/Service/Filter/FilterLoaderInterface.php @@ -0,0 +1,22 @@ +queryFactory->create($type); + foreach ($this->filterLoader->loadFilters() as $filter) { + $query = $filter->apply($collection, $query); + } + return $query; + } +} \ No newline at end of file diff --git a/src/Service/Filter/FilterServiceInterface.php b/src/Service/Filter/FilterServiceInterface.php new file mode 100644 index 000000000..616ca413e --- /dev/null +++ b/src/Service/Filter/FilterServiceInterface.php @@ -0,0 +1,29 @@ +mockAssetAdapterInterface()); + $this->expectExceptionMessage('Unknown query type: invalid'); + $queryFactory->create('invalid'); + } + + /** + * @throws InvalidQueryTypeException + * @throws Exception + */ + public function testAssetQueryType(): void + { + $queryFactory = new QueryFactory($this->mockAssetAdapterInterface()); + $query = $queryFactory->create('asset'); + + $this->assertInstanceOf(AssetQuery::class, $query); + } + + /** + * @throws Exception + */ + private function mockAssetAdapterInterface(): AssetQueryProviderInterface + { + return $this->makeEmpty(AssetQueryProviderInterface::class, [ + 'createAssetQuery' => function () { + return new AssetQuery($this->makeEmpty(SearchInterface::class)); + } + ]); + } +} \ No newline at end of file