diff --git a/src/Model/Product/BatchLoad/ProductBatchLoadByEntityData.php b/src/Model/Product/BatchLoad/ProductBatchLoadByEntityData.php index 946b3ab8a..57287cd07 100644 --- a/src/Model/Product/BatchLoad/ProductBatchLoadByEntityData.php +++ b/src/Model/Product/BatchLoad/ProductBatchLoadByEntityData.php @@ -10,8 +10,7 @@ class ProductBatchLoadByEntityData { /** * @param string $id - * @param int $entityId - * @param string $entityClass + * @param object $entity * @param int $limit * @param int $offset * @param string $orderingModeId @@ -20,8 +19,7 @@ class ProductBatchLoadByEntityData */ public function __construct( protected readonly string $id, - protected readonly int $entityId, - protected readonly string $entityClass, + protected readonly object $entity, protected readonly int $limit, protected readonly int $offset, protected readonly string $orderingModeId, @@ -39,19 +37,13 @@ public function getId(): string } /** - * @return int - */ - public function getEntityId(): int - { - return $this->entityId; - } - - /** - * @return string + * @template T of object + * @param class-string|null $entityClassName + * @return T */ - public function getEntityClass(): string + public function getEntity(string $entityClassName = null): object { - return $this->entityClass; + return $this->entity; } /** diff --git a/src/Model/Product/BatchLoad/ProductElasticsearchBatchProvider.php b/src/Model/Product/BatchLoad/ProductElasticsearchBatchProvider.php index 4819c0c2c..c434ec876 100644 --- a/src/Model/Product/BatchLoad/ProductElasticsearchBatchProvider.php +++ b/src/Model/Product/BatchLoad/ProductElasticsearchBatchProvider.php @@ -4,7 +4,14 @@ namespace Shopsys\FrontendApiBundle\Model\Product\BatchLoad; +use InvalidArgumentException; +use Shopsys\FrameworkBundle\Component\EntityExtension\EntityNameResolver; +use Shopsys\FrameworkBundle\Model\Category\AutomatedFilter\CategoryAutomatedFilterFacade; +use Shopsys\FrameworkBundle\Model\Category\Category; +use Shopsys\FrameworkBundle\Model\Product\Brand\Brand; +use Shopsys\FrameworkBundle\Model\Product\Flag\Flag; use Shopsys\FrameworkBundle\Model\Product\ProductFrontendLimitProvider; +use Shopsys\FrameworkBundle\Model\Product\Search\FilterQuery; use Shopsys\FrameworkBundle\Model\Product\Search\FilterQueryFactory; class ProductElasticsearchBatchProvider @@ -13,11 +20,15 @@ class ProductElasticsearchBatchProvider * @param \Shopsys\FrameworkBundle\Model\Product\Search\FilterQueryFactory $filterQueryFactory * @param \Shopsys\FrontendApiBundle\Model\Product\BatchLoad\ProductElasticsearchBatchRepository $productElasticsearchBatchRepository * @param \Shopsys\FrameworkBundle\Model\Product\ProductFrontendLimitProvider $productFrontendLimitProvider + * @param \Shopsys\FrameworkBundle\Component\EntityExtension\EntityNameResolver $entityNameResolver + * @param \Shopsys\FrameworkBundle\Model\Category\AutomatedFilter\CategoryAutomatedFilterFacade $categoryAutomatedFilterFacade */ public function __construct( protected readonly FilterQueryFactory $filterQueryFactory, protected readonly ProductElasticsearchBatchRepository $productElasticsearchBatchRepository, protected readonly ProductFrontendLimitProvider $productFrontendLimitProvider, + protected readonly EntityNameResolver $entityNameResolver, + protected readonly CategoryAutomatedFilterFacade $categoryAutomatedFilterFacade, ) { } @@ -50,4 +61,121 @@ public function getBatchedSellableByProductIds(array $productsIds): array return $this->productElasticsearchBatchRepository->getBatchedProductsAndTotalsByFilterQueries($filterQueries); } + + /** + * @param \Shopsys\FrontendApiBundle\Model\Product\BatchLoad\ProductSellableInCategoryBatchLoadData[] $sellableInCategoryBatchLoadData + * @return array + */ + public function getBatchedSellableInCategoryByIds(array $sellableInCategoryBatchLoadData): array + { + $filterQueries = []; + + foreach ($sellableInCategoryBatchLoadData as $batchLoadData) { + $filterQueries[] = $this->getSellableByProductsIdsInCategoryFilterQuery($batchLoadData->productIds, $batchLoadData->category); + } + + return $this->productElasticsearchBatchRepository->getBatchedProductsAndTotalsByFilterQueries($filterQueries); + } + + /** + * @param \Shopsys\FrontendApiBundle\Model\Product\BatchLoad\ProductBatchLoadByEntityData[] $productBatchLoadByEntitiesData + * @return array + */ + public function getBatchedByEntities(array $productBatchLoadByEntitiesData): array + { + $filterQueries = []; + + foreach ($productBatchLoadByEntitiesData as $productBatchLoadByEntityData) { + $filterQueries[$productBatchLoadByEntityData->getId()] = $this->getFilterQuery($productBatchLoadByEntityData); + } + + return $this->productElasticsearchBatchRepository->getBatchedProductsAndTotalsByFilterQueries($filterQueries); + } + + /** + * @param \Shopsys\FrontendApiBundle\Model\Product\BatchLoad\ProductBatchLoadByEntityData $productBatchLoadByEntityData + * @return \Shopsys\FrameworkBundle\Model\Product\Search\FilterQuery + */ + protected function getFilterQuery(ProductBatchLoadByEntityData $productBatchLoadByEntityData): FilterQuery + { + $entity = $productBatchLoadByEntityData->getEntity(); + + $filterQuery = match (true) { + $entity instanceof Category => $this->getFilterQueryForCategory($productBatchLoadByEntityData), + $entity instanceof Flag => $this->getFilterQueryForFilterData($productBatchLoadByEntityData), + $entity instanceof Brand => $this->getFilterQueryForBrand($productBatchLoadByEntityData), + default => throw new InvalidArgumentException(sprintf('Entity class "%s" is not supported for creating filter query', get_class($entity))), + }; + + $filterQuery = $filterQuery->setFrom($productBatchLoadByEntityData->getOffset()); + + if ($productBatchLoadByEntityData->getSearch() !== '') { + $filterQuery = $filterQuery->search($productBatchLoadByEntityData->getSearch()); + } + + return $filterQuery; + } + + /** + * @param \Shopsys\FrontendApiBundle\Model\Product\BatchLoad\ProductBatchLoadByEntityData $productBatchLoadByEntityData + * @return \Shopsys\FrameworkBundle\Model\Product\Search\FilterQuery + */ + protected function getFilterQueryForCategory( + ProductBatchLoadByEntityData $productBatchLoadByEntityData, + ): FilterQuery { + return $this->filterQueryFactory->createListableProductsByCategory( + $productBatchLoadByEntityData->getProductFilterData(), + $productBatchLoadByEntityData->getOrderingModeId(), + 1, + $productBatchLoadByEntityData->getLimit(), + $productBatchLoadByEntityData->getEntity($this->entityNameResolver->resolve(Category::class)), + ); + } + + /** + * @param \Shopsys\FrontendApiBundle\Model\Product\BatchLoad\ProductBatchLoadByEntityData $productBatchLoadByEntityData + * @return \Shopsys\FrameworkBundle\Model\Product\Search\FilterQuery + */ + protected function getFilterQueryForFilterData( + ProductBatchLoadByEntityData $productBatchLoadByEntityData, + ): FilterQuery { + return $this->filterQueryFactory->createWithProductFilterData( + $productBatchLoadByEntityData->getProductFilterData(), + $productBatchLoadByEntityData->getOrderingModeId(), + 1, + $productBatchLoadByEntityData->getLimit(), + ); + } + + /** + * @param \Shopsys\FrontendApiBundle\Model\Product\BatchLoad\ProductBatchLoadByEntityData $productBatchLoadByEntityData + * @return \Shopsys\FrameworkBundle\Model\Product\Search\FilterQuery + */ + protected function getFilterQueryForBrand(ProductBatchLoadByEntityData $productBatchLoadByEntityData): FilterQuery + { + return $this->filterQueryFactory->createListableProductsByBrand( + $productBatchLoadByEntityData->getProductFilterData(), + $productBatchLoadByEntityData->getOrderingModeId(), + 1, + $productBatchLoadByEntityData->getLimit(), + $productBatchLoadByEntityData->getEntity($this->entityNameResolver->resolve(Brand::class)), + ); + } + + /** + * @param array $productIds + * @param \Shopsys\FrameworkBundle\Model\Category\Category $category + * @return \Shopsys\FrameworkBundle\Model\Product\Search\FilterQuery + */ + protected function getSellableByProductsIdsInCategoryFilterQuery( + array $productIds, + Category $category, + ): FilterQuery { + $filterQuery = $this->filterQueryFactory->createSellableProductsByProductIdsFilter( + $productIds, + $this->productFrontendLimitProvider->getProductsFrontendLimit(), + ); + + return $this->categoryAutomatedFilterFacade->applyFiltersByCategory($filterQuery, $category); + } } diff --git a/src/Model/Product/BatchLoad/ProductSellableInCategoryBatchLoadData.php b/src/Model/Product/BatchLoad/ProductSellableInCategoryBatchLoadData.php new file mode 100644 index 000000000..760e246c0 --- /dev/null +++ b/src/Model/Product/BatchLoad/ProductSellableInCategoryBatchLoadData.php @@ -0,0 +1,20 @@ +promiseAdapter->all($this->productElasticsearchBatchProvider->getBatchedSellableByProductIds($productsIds)[ProductElasticsearchBatchRepository::TOTALS_KEY]); } + /** + * @param \Shopsys\FrontendApiBundle\Model\Product\BatchLoad\ProductSellableInCategoryBatchLoadData[] $sellableInCategoryBatchLoadData + * @return \GraphQL\Executor\Promise\Promise + */ + public function loadSellableInCategoryByIds(array $sellableInCategoryBatchLoadData): Promise + { + return $this->promiseAdapter->all($this->productElasticsearchBatchProvider->getBatchedSellableInCategoryByIds($sellableInCategoryBatchLoadData)[ProductElasticsearchBatchRepository::PRODUCTS_KEY]); + } + + /** + * @param \Shopsys\FrontendApiBundle\Model\Product\BatchLoad\ProductSellableInCategoryBatchLoadData[] $sellableInCategoryBatchLoadData + * @return \GraphQL\Executor\Promise\Promise + */ + public function loadSellableCountInCategoryByIds(array $sellableInCategoryBatchLoadData): Promise + { + return $this->promiseAdapter->all($this->productElasticsearchBatchProvider->getBatchedSellableInCategoryByIds($sellableInCategoryBatchLoadData)[ProductElasticsearchBatchRepository::TOTALS_KEY]); + } + /** * @param string $batchLoadDataId * @return int @@ -85,6 +103,24 @@ public static function getTotalByBatchLoadDataId(string $batchLoadDataId): int return self::$totalsIndexedByBatchLoadDataId[$batchLoadDataId] ?? 0; } + /** + * @param \Shopsys\FrontendApiBundle\Model\Product\BatchLoad\ProductBatchLoadByEntityData[] $productBatchLoadByEntitiesData + * @return \GraphQL\Executor\Promise\Promise + */ + public function loadByEntities(array $productBatchLoadByEntitiesData): Promise + { + $batchedByEntities = $this->productElasticsearchBatchProvider->getBatchedByEntities($productBatchLoadByEntitiesData); + self::$totalsIndexedByBatchLoadDataId = $batchedByEntities[ProductElasticsearchBatchRepository::TOTALS_KEY]; + + $result = []; + + foreach ($productBatchLoadByEntitiesData as $productBatchLoadByEntityData) { + $result[] = $batchedByEntities[ProductElasticsearchBatchRepository::PRODUCTS_KEY][$productBatchLoadByEntityData->getId()]; + } + + return $this->promiseAdapter->all($result); + } + /** * @param array $arrayForSorting * @param array $originalArray diff --git a/src/Model/Product/Filter/ProductFilterOptionsFactory.php b/src/Model/Product/Filter/ProductFilterOptionsFactory.php index dcec66224..3846bfedd 100644 --- a/src/Model/Product/Filter/ProductFilterOptionsFactory.php +++ b/src/Model/Product/Filter/ProductFilterOptionsFactory.php @@ -163,7 +163,6 @@ public function createProductFilterOptionsForAll( if ($searchText !== '') { $productFilterCountData = $this->productOnCurrentDomainElasticFacade->getProductFilterCountDataForSearch( $searchText, - $productFilterConfig, $productFilterData, ); } else { @@ -205,8 +204,7 @@ public function createProductFilterOptionsForCategory( } $productFilterCountData = $this->productOnCurrentDomainElasticFacade->getProductFilterCountDataInCategory( - $category->getId(), - $productFilterConfig, + $category, $productFilterData, ); diff --git a/src/Model/Product/ProductFacade.php b/src/Model/Product/ProductFacade.php index 9c38b9f61..480b5f375 100644 --- a/src/Model/Product/ProductFacade.php +++ b/src/Model/Product/ProductFacade.php @@ -4,9 +4,7 @@ namespace Shopsys\FrontendApiBundle\Model\Product; -use Shopsys\FrameworkBundle\Model\Category\Category; use Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroup; -use Shopsys\FrameworkBundle\Model\Product\Brand\Brand; use Shopsys\FrameworkBundle\Model\Product\Filter\ProductFilterData; use Shopsys\FrameworkBundle\Model\Product\Product; use Shopsys\FrameworkBundle\Model\Product\Search\FilterQueryFactory; @@ -86,116 +84,6 @@ public function getFilteredProductsOnCurrentDomain( return $productsResult->getHits(); } - /** - * @param \Shopsys\FrameworkBundle\Model\Category\Category $category - * @param int $limit - * @param int $offset - * @param string $orderingModeId - * @param \Shopsys\FrameworkBundle\Model\Product\Filter\ProductFilterData $productFilterData - * @param string $search - * @return array - */ - public function getFilteredProductsByCategory( - Category $category, - int $limit, - int $offset, - string $orderingModeId, - ProductFilterData $productFilterData, - string $search, - ): array { - $filterQuery = $this->filterQueryFactory->createListableProductsByCategoryId( - $productFilterData, - $orderingModeId, - 1, - $limit, - $category->getId(), - )->setFrom($offset); - - if ($search !== '') { - $filterQuery = $filterQuery->search($search); - } - - $productsResult = $this->productElasticsearchRepository->getSortedProductsResultByFilterQuery($filterQuery); - - return $productsResult->getHits(); - } - - /** - * @param \Shopsys\FrameworkBundle\Model\Category\Category $category - * @param \Shopsys\FrameworkBundle\Model\Product\Filter\ProductFilterData $productFilterData - * @param string $search - * @return int - */ - public function getFilteredProductsByCategoryCount( - Category $category, - ProductFilterData $productFilterData, - string $search, - ): int { - $filterQuery = $this->filterQueryFactory->createListableWithProductFilter($productFilterData) - ->filterByCategory([$category->getId()]); - - if ($search !== '') { - $filterQuery = $filterQuery->search($search); - } - - return $this->productElasticsearchRepository->getProductsCountByFilterQuery($filterQuery); - } - - /** - * @param \Shopsys\FrameworkBundle\Model\Product\Brand\Brand $brand - * @param int $limit - * @param int $offset - * @param string $orderingModeId - * @param \Shopsys\FrameworkBundle\Model\Product\Filter\ProductFilterData $productFilterData - * @param string $search - * @return array - */ - public function getFilteredProductsByBrand( - Brand $brand, - int $limit, - int $offset, - string $orderingModeId, - ProductFilterData $productFilterData, - string $search, - ): array { - $filterQuery = $this->filterQueryFactory->createListableProductsByBrandId( - $productFilterData, - $orderingModeId, - 1, - $limit, - $brand->getId(), - )->setFrom($offset); - - if ($search !== '') { - $filterQuery = $filterQuery->search($search); - } - - $productsResult = $this->productElasticsearchRepository->getSortedProductsResultByFilterQuery($filterQuery); - - return $productsResult->getHits(); - } - - /** - * @param \Shopsys\FrameworkBundle\Model\Product\Brand\Brand $brand - * @param \Shopsys\FrameworkBundle\Model\Product\Filter\ProductFilterData $productFilterData - * @param string $search - * @return int - */ - public function getFilteredProductsByBrandCount( - Brand $brand, - ProductFilterData $productFilterData, - string $search, - ): int { - $filterQuery = $this->filterQueryFactory->createListableWithProductFilter($productFilterData) - ->filterByBrands([$brand->getId()]); - - if ($search !== '') { - $filterQuery = $filterQuery->search($search); - } - - return $this->productElasticsearchRepository->getProductsCountByFilterQuery($filterQuery); - } - /** * @param array $productIds * @param int|null $limit diff --git a/src/Model/Resolver/Category/CategoryResolverMap.php b/src/Model/Resolver/Category/CategoryResolverMap.php index a4324c929..c6d7394ee 100644 --- a/src/Model/Resolver/Category/CategoryResolverMap.php +++ b/src/Model/Resolver/Category/CategoryResolverMap.php @@ -73,6 +73,7 @@ protected function mapCommonFields(string $fieldName, Category $category): mixed 'linkedCategories' => $this->linkedCategoriesBatchLoader->load($category), 'categoryHierarchy' => $this->categoryFacade->getVisibleCategoriesInPathFromRootOnDomain($category, $this->domain->getId()), 'hreflangLinks' => $this->hreflangLinksFacade->getForCategory($category, $this->domain->getId()), + 'automatedFilters' => $category->getAutomatedFilters(), default => throw new InvalidArgumentException(sprintf('Unknown field name "%s".', $fieldName)), }; } diff --git a/src/Model/Resolver/Products/BestsellingProductsQuery.php b/src/Model/Resolver/Products/BestsellingProductsQuery.php index e5192c1a2..41be6aebf 100644 --- a/src/Model/Resolver/Products/BestsellingProductsQuery.php +++ b/src/Model/Resolver/Products/BestsellingProductsQuery.php @@ -12,6 +12,7 @@ use Shopsys\FrameworkBundle\Model\Customer\User\CurrentCustomerUser; use Shopsys\FrameworkBundle\Model\Product\BestsellingProduct\CachedBestsellingProductFacade; use Shopsys\FrameworkBundle\Model\Product\ProductFrontendLimitProvider; +use Shopsys\FrontendApiBundle\Model\Product\BatchLoad\ProductSellableInCategoryBatchLoadDataFactory; use Shopsys\FrontendApiBundle\Model\Resolver\AbstractQuery; class BestsellingProductsQuery extends AbstractQuery @@ -20,15 +21,17 @@ class BestsellingProductsQuery extends AbstractQuery * @param \Shopsys\FrameworkBundle\Model\Product\BestsellingProduct\CachedBestsellingProductFacade $cachedBestsellingProductFacade * @param \Shopsys\FrameworkBundle\Component\Domain\Domain $domain * @param \Shopsys\FrameworkBundle\Model\Customer\User\CurrentCustomerUser $currentCustomerUser - * @param \Overblog\DataLoader\DataLoaderInterface $productsSellableByIdsBatchLoader + * @param \Overblog\DataLoader\DataLoaderInterface $productsSellableInCategoryByIdsBatchLoader * @param \Shopsys\FrameworkBundle\Model\Product\ProductFrontendLimitProvider $productFrontendLimitProvider + * @param \Shopsys\FrontendApiBundle\Model\Product\BatchLoad\ProductSellableInCategoryBatchLoadDataFactory $productSellableInCategoryBatchLoadDataFactory */ public function __construct( protected readonly CachedBestsellingProductFacade $cachedBestsellingProductFacade, protected readonly Domain $domain, protected readonly CurrentCustomerUser $currentCustomerUser, - protected readonly DataLoaderInterface $productsSellableByIdsBatchLoader, + protected readonly DataLoaderInterface $productsSellableInCategoryByIdsBatchLoader, protected readonly ProductFrontendLimitProvider $productFrontendLimitProvider, + protected readonly ProductSellableInCategoryBatchLoadDataFactory $productSellableInCategoryBatchLoadDataFactory, ) { } @@ -50,6 +53,11 @@ public function bestSellingProductsByCategoryQuery( $this->productFrontendLimitProvider->getProductsFrontendLimit(), ); - return $this->productsSellableByIdsBatchLoader->load($bestsellingProductsIds); + $batchLoadData = $this->productSellableInCategoryBatchLoadDataFactory->create( + $bestsellingProductsIds, + $category, + ); + + return $this->productsSellableInCategoryByIdsBatchLoader->load($batchLoadData); } } diff --git a/src/Model/Resolver/Products/DataMapper/ProductArrayFieldMapper.php b/src/Model/Resolver/Products/DataMapper/ProductArrayFieldMapper.php index 6e9638661..3ef2dc038 100644 --- a/src/Model/Resolver/Products/DataMapper/ProductArrayFieldMapper.php +++ b/src/Model/Resolver/Products/DataMapper/ProductArrayFieldMapper.php @@ -129,7 +129,7 @@ public function getBrand(array $data): ?Brand */ public function isSellingDenied(array $data): bool { - return $data['calculated_selling_denied']; + return $data['calculated_selling_denied'] === true || $data['is_sale_exclusion'] === true; } /** diff --git a/src/Model/Resolver/Products/ProductsQuery.php b/src/Model/Resolver/Products/ProductsQuery.php index e4589d0e3..a3fe4c8ae 100644 --- a/src/Model/Resolver/Products/ProductsQuery.php +++ b/src/Model/Resolver/Products/ProductsQuery.php @@ -84,8 +84,7 @@ function ($offset, $limit) use ($argument, $productFilterData, $brand, $batchLoa return $this->productsByEntitiesBatchLoader->load( new ProductBatchLoadByEntityData( $batchLoadDataId, - $brand->getId(), - Brand::class, + $brand, $limit, $offset, $this->productOrderingModeProvider->getOrderingModeFromArgument($argument), @@ -187,8 +186,7 @@ function ($offset, $limit) use ($argument, $category, $productFilterData, $order return $this->productsByEntitiesBatchLoader->load( new ProductBatchLoadByEntityData( $batchLoadDataId, - $category->getId(), - Category::class, + $category, $limit, $offset, $orderingMode, @@ -231,8 +229,7 @@ function ($offset, $limit) use ($argument, $productFilterData, $flag, $batchLoad return $this->productsByEntitiesBatchLoader->load( new ProductBatchLoadByEntityData( $batchLoadDataId, - $flag->getId(), - Flag::class, + $flag, $limit, $offset, $this->productOrderingModeProvider->getOrderingModeFromArgument($argument), diff --git a/src/Resources/config/graphql-types/EnumType/CategoryAutomatedFilterEnumDecorator.types.yaml b/src/Resources/config/graphql-types/EnumType/CategoryAutomatedFilterEnumDecorator.types.yaml new file mode 100644 index 000000000..5b7c7b4fe --- /dev/null +++ b/src/Resources/config/graphql-types/EnumType/CategoryAutomatedFilterEnumDecorator.types.yaml @@ -0,0 +1,10 @@ +CategoryAutomatedFilterEnumDecorator: + type: enum + decorator: true + config: + description: "Category automated filter types" + values: + onStock: + value: '@=constant("Shopsys\\FrameworkBundle\\Model\\Category\\AutomatedFilter\\OnStockCategoryAutomatedFilter::DATABASE_VALUE")' + newProducts: + value: '@=constant("Shopsys\\FrameworkBundle\\Model\\Category\\AutomatedFilter\\NewProductsCategoryAutomatedFilter::DATABASE_VALUE")' diff --git a/src/Resources/config/graphql-types/ModelType/Category/CategoryDecorator.types.yaml b/src/Resources/config/graphql-types/ModelType/Category/CategoryDecorator.types.yaml index b26a27cec..ec634f3a7 100644 --- a/src/Resources/config/graphql-types/ModelType/Category/CategoryDecorator.types.yaml +++ b/src/Resources/config/graphql-types/ModelType/Category/CategoryDecorator.types.yaml @@ -85,3 +85,6 @@ CategoryDecorator: linkedCategories: type: "[Category!]!" description: "A list of categories linked to the given category" + automatedFilters: + type: "[CategoryAutomatedFilterEnum!]!" + description: "Automated filters for the category" diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 3ba08b20a..fe63e3e35 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -149,7 +149,7 @@ services: Shopsys\FrontendApiBundle\Model\Resolver\Products\BestsellingProductsQuery: arguments: - $productsSellableByIdsBatchLoader: '@products_sellable_by_ids_batch_loader' + $productsSellableInCategoryByIdsBatchLoader: '@products_sellable_in_category_by_ids_batch_loader' Shopsys\FrontendApiBundle\Model\Resolver\Products\DataMapper\ProductArrayFieldMapper: arguments: