Skip to content

Commit

Permalink
Merge pull request #2706 from magento-architects/MAGETWO-92773-Anchor…
Browse files Browse the repository at this point in the history
…-Category

[architects] MAGETWO-92773: [GraphQL] Products cannot be fetched in parent/anchor category #89
  • Loading branch information
paliarush authored Jun 21, 2018
2 parents c2c4f29 + c47e1c9 commit 8eb668b
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,10 @@
use Magento\Framework\Reflection\DataObjectProcessor;

/**
* Category field resolver, used for GraphQL request processing.
* Resolver for category objects the product is assigned to.
*/
class Category implements ResolverInterface
class Categories implements ResolverInterface
{
/**
* Product category ids
*/
const PRODUCT_CATEGORY_IDS_KEY = 'category_ids';

/**
* @var Collection
*/
Expand Down Expand Up @@ -89,10 +84,13 @@ public function __construct(
*/
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value
{
$this->categoryIds = array_merge($this->categoryIds, $value[self::PRODUCT_CATEGORY_IDS_KEY]);
/** @var \Magento\Catalog\Model\Product $product */
$product = $value['model'];
$categoryIds = $product->getCategoryIds();
$this->categoryIds = array_merge($this->categoryIds, $categoryIds);
$that = $this;

return $this->valueFactory->create(function () use ($that, $value, $info) {
return $this->valueFactory->create(function () use ($that, $categoryIds, $info) {
$categories = [];
if (empty($that->categoryIds)) {
return [];
Expand All @@ -104,7 +102,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
}
/** @var CategoryInterface | \Magento\Catalog\Model\Category $item */
foreach ($this->collection as $item) {
if (in_array($item->getId(), $value[$that::PRODUCT_CATEGORY_IDS_KEY])) {
if (in_array($item->getId(), $categoryIds)) {
$categories[$item->getId()] = $this->dataObjectProcessor->buildOutputDataArray(
$item,
CategoryInterface::class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public function resolve(
array $args = null
): Value {
$args['filter'] = [
'category_ids' => [
'category_id' => [
'eq' => $value['id']
]
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,14 @@ public function getList(
$this->collectionProcessor->process($collection, $searchCriteria, $attributes);

if (!$isChildSearch) {
$visibilityIds
= $isSearch ? $this->visibility->getVisibleInSearchIds() : $this->visibility->getVisibleInCatalogIds();
$visibilityIds = $isSearch
? $this->visibility->getVisibleInSearchIds()
: $this->visibility->getVisibleInCatalogIds();
$collection->setVisibility($visibilityIds);
}
$collection->load();

// Methods that perform extra fetches post-load
$collection->addCategoryIds();
$collection->addMediaGalleryData();
$collection->addOptionsToResult();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,18 @@ class ProductEntityAttributesForAst implements FieldEntityAttributesInterface
/**
* @var array
*/
private $additionalAttributes;
private $additionalAttributes = ['min_price', 'max_price', 'category_id'];

/**
* @param ConfigInterface $config
* @param array $additionalAttributes
*/
public function __construct(
ConfigInterface $config,
array $additionalAttributes = ['min_price', 'max_price', 'category_ids']
array $additionalAttributes = []
) {
$this->config = $config;
$this->additionalAttributes = $additionalAttributes;
$this->additionalAttributes = array_merge($this->additionalAttributes, $additionalAttributes);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\CatalogGraphQl\Model\Resolver\Products\SearchCriteria\CollectionProcessor\FilterProcessor;

use Magento\Catalog\Model\CategoryFactory;
use Magento\Catalog\Model\ResourceModel\Category as CategoryResourceModel;
use Magento\Catalog\Model\ResourceModel\Product\Collection;
use Magento\Framework\Api\Filter;
use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface;
use Magento\Framework\Data\Collection\AbstractDb;
use Magento\Framework\Exception\LocalizedException;

/**
* Category filter allows to filter products collection using 'category_id' filter from search criteria.
*/
class CategoryFilter implements CustomFilterInterface
{
/**
* @var CategoryFactory
*/
private $categoryFactory;

/**
* @var CategoryResourceModel
*/
private $categoryResourceModel;

/**
* @param CategoryFactory $categoryFactory
* @param CategoryResourceModel $categoryResourceModel
*/
public function __construct(
CategoryFactory $categoryFactory,
CategoryResourceModel $categoryResourceModel
) {
$this->categoryFactory = $categoryFactory;
$this->categoryResourceModel = $categoryResourceModel;
}

/**
* Apply filter by 'category_id' to product collection.
*
* For anchor categories, the products from all children categories will be present in the result.
*
* @param Filter $filter
* @param AbstractDb $collection
* @return bool Whether the filter is applied
* @throws LocalizedException
*/
public function apply(Filter $filter, AbstractDb $collection)
{
$conditionType = $filter->getConditionType();

if ($conditionType !== 'eq') {
throw new LocalizedException(__("'category_id' only supports 'eq' condition type."));
}

$categoryId = $filter->getValue();
/** @var Collection $collection */
$category = $this->categoryFactory->create();
$this->categoryResourceModel->load($category, $categoryId);
$collection->addCategoryFilter($category);

return true;
}
}
2 changes: 1 addition & 1 deletion app/code/Magento/CatalogGraphQl/etc/graphql/di.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
<item name="price" xsi:type="object">Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor\ProductPriceFilter</item>
<item name="min_price" xsi:type="object">Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor\ProductPriceFilter</item>
<item name="max_price" xsi:type="object">Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor\ProductPriceFilter</item>
<item name="category_ids" xsi:type="object">Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor\ProductCategoryFilter</item>
<item name="category_id" xsi:type="object">Magento\CatalogGraphQl\Model\Resolver\Products\SearchCriteria\CollectionProcessor\FilterProcessor\CategoryFilter</item>
</argument>
</arguments>
</virtualType>
Expand Down
4 changes: 2 additions & 2 deletions app/code/Magento/CatalogGraphQl/etc/schema.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\
price: ProductPrices @doc(description: "A ProductPrices object, indicating the price of an item") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Price")
gift_message_available: String @doc(description: "Indicates whether a gift message is available")
manufacturer: Int @doc(description: "A number representing the product's manufacturer")
categories: [CategoryInterface] @doc(description: "The categories assigned to a product") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category")
categories: [CategoryInterface] @doc(description: "The categories assigned to a product") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Categories")
canonical_url: String @doc(description: "Canonical URL") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CanonicalUrl")
}

Expand Down Expand Up @@ -441,7 +441,7 @@ input ProductFilterInput @doc(description: "ProductFilterInput defines the filte
min_price: FilterTypeInput @doc(description:"The numeric minimal price of the product. Do not include the currency code.")
max_price: FilterTypeInput @doc(description:"The numeric maximal price of the product. Do not include the currency code.")
special_price: FilterTypeInput @doc(description:"The numeric special price of the product. Do not include the currency code.")
category_ids: FilterTypeInput @doc(description: "An array of category IDs the product belongs to")
category_id: FilterTypeInput @doc(description: "Category ID the product belongs to")
options_container: FilterTypeInput @doc(description: "If the product has multiple options, determines where they appear on the product page")
required_options: FilterTypeInput @doc(description: "Indicates whether the product has required options")
has_options: FilterTypeInput @doc(description: "Indicates whether additional attributes have been created for the product")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace Magento\GraphQl\Catalog;

use Magento\Catalog\Api\Data\CategoryInterface;
use Magento\Framework\DataObject;
use Magento\TestFramework\TestCase\GraphQlAbstract;
use Magento\Catalog\Api\Data\ProductInterface;
Expand Down Expand Up @@ -254,7 +255,6 @@ public function testCategoryProducts()
default_group_id
is_default
}
}
}
}
Expand All @@ -281,6 +281,54 @@ public function testCategoryProducts()
$this->assertWebsites($firstProduct, $response['category']['products']['items'][0]['websites']);
}

/**
* @magentoApiDataFixture Magento/Catalog/_files/categories.php
*/
public function testAnchorCategory()
{
/** @var \Magento\Catalog\Model\ResourceModel\Category\Collection $categoryCollection */
$categoryCollection = $this->objectManager->create(
\Magento\Catalog\Model\ResourceModel\Category\Collection::class
);
$categoryCollection->addFieldToFilter('name', 'Category 1');
$category = $categoryCollection->getFirstItem();
/** @var \Magento\Framework\EntityManager\MetadataPool $entityManagerMetadataPool */
$entityManagerMetadataPool = $this->objectManager->create(\Magento\Framework\EntityManager\MetadataPool::class);
$categoryLinkField = $entityManagerMetadataPool->getMetadata(CategoryInterface::class)->getLinkField();
$categoryId = $category->getData($categoryLinkField);
$this->assertNotEmpty($categoryId, "Preconditions failed: category is not available.");

$query = <<<QUERY
{
category(id: {$categoryId}) {
name
products(sort: {sku: ASC}) {
total_count
items {
sku
}
}
}
}
QUERY;

$response = $this->graphQlQuery($query);
$expectedResponse = [
'category' => [
'name' => 'Category 1',
'products' => [
'total_count' => 3,
'items' => [
['sku' => '12345'],
['sku' => 'simple'],
['sku' => 'simple-4']
]
]
]
];
$this->assertEquals($expectedResponse, $response);
}

/**
* @param ProductInterface $product
* @param array $actualResponse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ public function testFilterProductsByCategoryIds()
products(
filter:
{
category_ids:{eq:"{$queryCategoryId}"}
category_id:{eq:"{$queryCategoryId}"}
}
pageSize:2
Expand Down

0 comments on commit 8eb668b

Please sign in to comment.