From 62dd8f51c6fb2217ed8af94547a9b81dcdd908c1 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Tue, 6 Aug 2019 10:40:39 -0500 Subject: [PATCH 001/147] Add custom attributes to products filter and sort input --- .../Model/Config/FilterAttributeReader.php | 98 +++++++++++++++++++ .../Model/Config/SortAttributeReader.php | 79 +++++++++++++++ app/code/Magento/CatalogGraphQl/etc/di.xml | 2 + .../Magento/CatalogGraphQl/etc/graphql/di.xml | 6 ++ 4 files changed, 185 insertions(+) create mode 100644 app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php create mode 100644 app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php new file mode 100644 index 0000000000000..b84c18ea5ac0b --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php @@ -0,0 +1,98 @@ +mapper = $mapper; + $this->collectionFactory = $collectionFactory; + } + + /** + * Read configuration scope + * + * @param string|null $scope + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function read($scope = null) : array + { + $typeNames = $this->mapper->getMappedTypes(self::ENTITY_TYPE); + $config = []; + + foreach ($this->getAttributeCollection() as $attribute) { + $attributeCode = $attribute->getAttributeCode(); + + foreach ($typeNames as $typeName) { + $config[$typeName]['fields'][$attributeCode] = [ + 'name' => $attributeCode, + 'type' => self::FILTER_TYPE, + 'arguments' => [], + 'required' => false, + 'description' => sprintf('Attribute label: %s', $attribute->getDefaultFrontendLabel()) + ]; + } + } + + return $config; + } + + /** + * Create attribute collection + * + * @return Collection|\Magento\Catalog\Model\ResourceModel\Eav\Attribute[] + */ + private function getAttributeCollection() + { + return $this->collectionFactory->create() + ->addHasOptionsFilter() + ->addIsSearchableFilter() + ->addDisplayInAdvancedSearchFilter(); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php new file mode 100644 index 0000000000000..215b28be0579c --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php @@ -0,0 +1,79 @@ +mapper = $mapper; + $this->attributesCollection = $attributesCollection; + } + + /** + * Read configuration scope + * + * @param string|null $scope + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function read($scope = null) : array + { + $map = $this->mapper->getMappedTypes(self::ENTITY_TYPE); + $config =[]; + $attributes = $this->attributesCollection->addSearchableAttributeFilter()->addFilter('used_for_sort_by', 1); + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ + foreach ($attributes as $attribute) { + $attributeCode = $attribute->getAttributeCode(); + $attributeLabel = $attribute->getDefaultFrontendLabel(); + foreach ($map as $type) { + $config[$type]['fields'][$attributeCode] = [ + 'name' => $attributeCode, + 'type' => self::FIELD_TYPE, + 'arguments' => [], + 'required' => false, + 'description' => __('Attribute label: ') . $attributeLabel + ]; + } + } + + return $config; + } +} diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml index a5006355ed265..cea1c7ce327e2 100644 --- a/app/code/Magento/CatalogGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/di.xml @@ -19,6 +19,8 @@ Magento\CatalogGraphQl\Model\Config\AttributeReader Magento\CatalogGraphQl\Model\Config\CategoryAttributeReader + Magento\CatalogGraphQl\Model\Config\SortAttributeReader + Magento\CatalogGraphQl\Model\Config\FilterAttributeReader diff --git a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml index 2292004f3cf01..8fe3d19619fc3 100644 --- a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml @@ -48,6 +48,12 @@ CustomizableRadioOption CustomizableCheckboxOption + + ProductSortInput + + + ProductFilterInput + From 10796a3ad68b74579deb8fe034cc3bdabc997301 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Wed, 7 Aug 2019 11:35:36 -0500 Subject: [PATCH 002/147] MC-18988: Investigate layer navigation code in lightweight resolver implementation --- .../Model/Resolver/LayerFilters.php | 19 +++++++++-- .../Model/Resolver/Products.php | 4 ++- .../Model/Resolver/Products/Query/Search.php | 32 +++++++++++++------ .../CatalogGraphQl/etc/schema.graphqls | 8 +++++ .../Query/Resolver/Argument/AstConverter.php | 13 +++++++- 5 files changed, 61 insertions(+), 15 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php index 0ec7e12e42d55..d493c7ba8e66f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php @@ -10,6 +10,7 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +//use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\LayerBuilder; /** * Layered navigation filters resolver, used for GraphQL request processing. @@ -21,13 +22,24 @@ class LayerFilters implements ResolverInterface */ private $filtersDataProvider; +// /** +// * @var LayerBuilder +// */ +// private $layerBuilder; + /** * @param \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider + * @param \Magento\Framework\Registry $registry */ public function __construct( - \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider + \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider, + \Magento\Framework\Registry $registry + //LayerBuilder $layerBuilder + ) { $this->filtersDataProvider = $filtersDataProvider; + $this->registry = $registry; + //$this->layerBuilder = $layerBuilder; } /** @@ -43,7 +55,8 @@ public function resolve( if (!isset($value['layer_type'])) { return null; } - - return $this->filtersDataProvider->getData($value['layer_type']); + $aggregations = $this->registry->registry('aggregations'); + return []; + //return $this->layerBuilder->build($aggregations, 1); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php index a75a9d2cf50a0..93ecf9c881548 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php @@ -89,7 +89,8 @@ public function resolve( $searchResult = $this->searchQuery->getResult($searchCriteria, $info); } else { $layerType = Resolver::CATALOG_LAYER_CATEGORY; - $searchResult = $this->filterQuery->getResult($searchCriteria, $info); + $searchCriteria->setRequestName('catalog_view_container'); + $searchResult = $this->searchQuery->getResult($searchCriteria, $info); } //possible division by 0 if ($searchCriteria->getPageSize()) { @@ -116,6 +117,7 @@ public function resolve( 'current_page' => $currentPage, 'total_pages' => $maxPages ], + //'filters' => $aggregations 'layer_type' => $layerType ]; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index bc40c664425ff..7a766269af602 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -13,6 +13,7 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResultFactory; use Magento\Search\Api\SearchInterface; +use Magento\Framework\Api\Search\SearchCriteriaInterfaceFactory; /** * Full text search for catalog using given search criteria. @@ -49,6 +50,11 @@ class Search */ private $pageSizeProvider; + /** + * @var SearchCriteriaInterfaceFactory + */ + private $searchCriteriaFactory; + /** * @param SearchInterface $search * @param FilterHelper $filterHelper @@ -56,6 +62,8 @@ class Search * @param SearchResultFactory $searchResultFactory * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool * @param \Magento\Search\Model\Search\PageSizeProvider $pageSize + * @param SearchCriteriaInterfaceFactory $searchCriteriaFactory + * @param \Magento\Framework\Registry $registry */ public function __construct( SearchInterface $search, @@ -63,7 +71,9 @@ public function __construct( Filter $filterQuery, SearchResultFactory $searchResultFactory, \Magento\Framework\EntityManager\MetadataPool $metadataPool, - \Magento\Search\Model\Search\PageSizeProvider $pageSize + \Magento\Search\Model\Search\PageSizeProvider $pageSize, + SearchCriteriaInterfaceFactory $searchCriteriaFactory, + \Magento\Framework\Registry $registry ) { $this->search = $search; $this->filterHelper = $filterHelper; @@ -71,6 +81,8 @@ public function __construct( $this->searchResultFactory = $searchResultFactory; $this->metadataPool = $metadataPool; $this->pageSizeProvider = $pageSize; + $this->searchCriteriaFactory = $searchCriteriaFactory; + $this->registry = $registry; } /** @@ -89,11 +101,11 @@ public function getResult(SearchCriteriaInterface $searchCriteria, ResolveInfo $ $realPageSize = $searchCriteria->getPageSize(); $realCurrentPage = $searchCriteria->getCurrentPage(); // Current page must be set to 0 and page size to max for search to grab all ID's as temporary workaround - $pageSize = $this->pageSizeProvider->getMaxPageSize(); - $searchCriteria->setPageSize($pageSize); + //$pageSize = $this->pageSizeProvider->getMaxPageSize(); + $searchCriteria->setPageSize(10000); $searchCriteria->setCurrentPage(0); $itemsResults = $this->search->search($searchCriteria); - + $this->registry->register('aggregations', $itemsResults->getAggregations()); $ids = []; $searchIds = []; foreach ($itemsResults->getItems() as $item) { @@ -101,14 +113,14 @@ public function getResult(SearchCriteriaInterface $searchCriteria, ResolveInfo $ $searchIds[] = $item->getId(); } + $searchCriteriaIds = $this->searchCriteriaFactory->create(); $filter = $this->filterHelper->generate($idField, 'in', $searchIds); - $searchCriteria = $this->filterHelper->remove($searchCriteria, 'search_term'); - $searchCriteria = $this->filterHelper->add($searchCriteria, $filter); - $searchResult = $this->filterQuery->getResult($searchCriteria, $info, true); + $searchCriteriaIds = $this->filterHelper->add($searchCriteriaIds, $filter); + $searchResult = $this->filterQuery->getResult($searchCriteriaIds, $info, true); - $searchCriteria->setPageSize($realPageSize); - $searchCriteria->setCurrentPage($realCurrentPage); - $paginatedProducts = $this->paginateList($searchResult, $searchCriteria); + $searchCriteriaIds->setPageSize($realPageSize); + $searchCriteriaIds->setCurrentPage($realCurrentPage); + $paginatedProducts = $this->paginateList($searchResult, $searchCriteriaIds); $products = []; if (!isset($searchCriteria->getSortOrders()[0])) { diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index ea56faf94408e..0b71b849d09a4 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -315,6 +315,14 @@ input ProductFilterInput @doc(description: "ProductFilterInput defines the filte country_of_manufacture: FilterTypeInput @doc(description: "The product's country of origin.") custom_layout: FilterTypeInput @doc(description: "The name of a custom layout.") gift_message_available: FilterTypeInput @doc(description: "Indicates whether a gift message is available.") + cat: FilterTypeInput @doc(description: "CATEGORY attribute name") + mysize: FilterTypeInput @doc(description: "size attribute name") + mycolor: FilterTypeInput @doc(description: "color attribute name") + ca_1_631447041: FilterTypeInput @doc(description: "CATEGORY attribute name") + attributeset2attribute1: FilterTypeInput @doc(description: "CATEGORY attribute name") + visibility: FilterTypeInput @doc(description: "CATEGORY attribute name") + price_dynamic_algorithm: FilterTypeInput @doc(description: "CATEGORY attribute name") + category_ids: FilterTypeInput @doc(description: "CATEGORY attribute name") or: ProductFilterInput @doc(description: "The keyword required to perform a logical OR comparison.") } diff --git a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/AstConverter.php b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/AstConverter.php index 0b03fc509c786..802d46eafcb97 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/AstConverter.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/AstConverter.php @@ -54,10 +54,19 @@ public function __construct( * @param string $fieldName * @param array $arguments * @return array + * @throws \LogicException */ public function getClausesFromAst(string $fieldName, array $arguments) : array { $attributes = $this->fieldEntityAttributesPool->getEntityAttributesForEntityFromField($fieldName); + $attributes[] = 'cat'; + $attributes[] = 'mysize'; + $attributes[] = 'mycolor'; + $attributes[] = 'ca_1_631447041'; + $attributes[] = 'attributeset2attribute1'; + $attributes[] = 'price_dynamic_algorithm'; + $attributes[] = 'visibility'; + $attributes[] = 'category_ids'; $conditions = []; foreach ($arguments as $argumentName => $argument) { if (in_array($argumentName, $attributes)) { @@ -76,12 +85,14 @@ public function getClausesFromAst(string $fieldName, array $arguments) : array $value ); } - } else { + } elseif (is_array($argument)) { $conditions[] = $this->connectiveFactory->create( $this->getClausesFromAst($fieldName, $argument), $argumentName ); + } else { + throw new \LogicException('Attribute not found in the visible attributes list'); } } return $conditions; From a2156e1f623d99357c8b22f26fcc2fcc4bace3d7 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Thu, 8 Aug 2019 16:20:14 -0500 Subject: [PATCH 003/147] MC-18988: Investigate layer navigation code in lightweight resolver implementation --- .../DataProvider/AttributeQuery.php | 354 ++++++++++++++++++ .../Category/Query/CategoryAttributeQuery.php | 58 +++ .../DataProvider/CategoryAttributesMapper.php | 114 ++++++ .../AttributeOptionProvider.php | 107 ++++++ .../LayeredNavigation/Builder/Attribute.php | 179 +++++++++ .../LayeredNavigation/Builder/Category.php | 173 +++++++++ .../LayeredNavigation/Builder/Price.php | 107 ++++++ .../LayeredNavigation/LayerBuilder.php | 43 +++ .../LayerBuilderInterface.php | 40 ++ .../RootCategoryProvider.php | 55 +++ .../Model/Resolver/LayerFilters.php | 24 +- .../Magento/CatalogGraphQl/etc/graphql/di.xml | 10 + 12 files changed, 1254 insertions(+), 10 deletions(-) create mode 100644 app/code/Magento/CatalogGraphQl/DataProvider/AttributeQuery.php create mode 100644 app/code/Magento/CatalogGraphQl/DataProvider/Category/Query/CategoryAttributeQuery.php create mode 100644 app/code/Magento/CatalogGraphQl/DataProvider/CategoryAttributesMapper.php create mode 100644 app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php create mode 100644 app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php create mode 100644 app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php create mode 100644 app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php create mode 100644 app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php create mode 100644 app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilderInterface.php create mode 100644 app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/RootCategoryProvider.php diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/AttributeQuery.php b/app/code/Magento/CatalogGraphQl/DataProvider/AttributeQuery.php new file mode 100644 index 0000000000000..b0f085932bb8e --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/AttributeQuery.php @@ -0,0 +1,354 @@ +resourceConnection = $resourceConnection; + $this->metadataPool = $metadataPool; + $this->entityType = $entityType; + $this->linkedAttributes = $linkedAttributes; + $this->eavConfig = $eavConfig; + } + + /** + * Form and return query to get eav entity $attributes for given $entityIds. + * + * If eav entities were not found, then data is fetching from $entityTableName. + * + * @param array $entityIds + * @param array $attributes + * @param int $storeId + * @return Select + * @throws \Zend_Db_Select_Exception + * @throws \Exception + */ + public function getQuery(array $entityIds, array $attributes, int $storeId): Select + { + /** @var \Magento\Framework\EntityManager\EntityMetadataInterface $metadata */ + $metadata = $this->metadataPool->getMetadata($this->entityType); + $entityTableName = $metadata->getEntityTable(); + + /** @var \Magento\Framework\DB\Adapter\AdapterInterface $connection */ + $connection = $this->resourceConnection->getConnection(); + $entityTableAttributes = \array_keys($connection->describeTable($entityTableName)); + + $attributeMetadataTable = $this->resourceConnection->getTableName('eav_attribute'); + $eavAttributes = $this->getEavAttributeCodes($attributes, $entityTableAttributes); + $entityTableAttributes = \array_intersect($attributes, $entityTableAttributes); + + $eavAttributesMetaData = $this->getAttributesMetaData($connection, $attributeMetadataTable, $eavAttributes); + + if ($eavAttributesMetaData) { + $select = $this->getEavAttributes( + $connection, + $metadata, + $entityTableAttributes, + $entityIds, + $eavAttributesMetaData, + $entityTableName, + $storeId + ); + } else { + $select = $this->getAttributesFromEntityTable( + $connection, + $entityTableAttributes, + $entityIds, + $entityTableName + ); + } + + return $select; + } + + /** + * Form and return query to get entity $entityTableAttributes for given $entityIds + * + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection + * @param array $entityTableAttributes + * @param array $entityIds + * @param string $entityTableName + * @return Select + */ + private function getAttributesFromEntityTable( + \Magento\Framework\DB\Adapter\AdapterInterface $connection, + array $entityTableAttributes, + array $entityIds, + string $entityTableName + ): Select { + $select = $connection->select() + ->from(['e' => $entityTableName], $entityTableAttributes) + ->where('e.entity_id IN (?)', $entityIds); + + return $select; + } + + /** + * Return ids of eav attributes by $eavAttributeCodes. + * + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection + * @param string $attributeMetadataTable + * @param array $eavAttributeCodes + * @return array + */ + private function getAttributesMetaData( + \Magento\Framework\DB\Adapter\AdapterInterface $connection, + string $attributeMetadataTable, + array $eavAttributeCodes + ): array { + $eavAttributeIdsSelect = $connection->select() + ->from(['a' => $attributeMetadataTable], ['attribute_id', 'backend_type', 'attribute_code']) + ->where('a.attribute_code IN (?)', $eavAttributeCodes) + ->where('a.entity_type_id = ?', $this->getEntityTypeId()); + + return $connection->fetchAssoc($eavAttributeIdsSelect); + } + + /** + * Form and return query to get eav entity $attributes for given $entityIds. + * + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection + * @param \Magento\Framework\EntityManager\EntityMetadataInterface $metadata + * @param array $entityTableAttributes + * @param array $entityIds + * @param array $eavAttributesMetaData + * @param string $entityTableName + * @param int $storeId + * @return Select + * @throws \Zend_Db_Select_Exception + */ + private function getEavAttributes( + \Magento\Framework\DB\Adapter\AdapterInterface $connection, + \Magento\Framework\EntityManager\EntityMetadataInterface $metadata, + array $entityTableAttributes, + array $entityIds, + array $eavAttributesMetaData, + string $entityTableName, + int $storeId + ): Select { + $selects = []; + $attributeValueExpression = $connection->getCheckSql( + $connection->getIfNullSql('store_eav.value_id', -1) . ' > 0', + 'store_eav.value', + 'eav.value' + ); + $linkField = $metadata->getLinkField(); + $attributesPerTable = $this->getAttributeCodeTables($entityTableName, $eavAttributesMetaData); + foreach ($attributesPerTable as $attributeTable => $eavAttributes) { + $attributeCodeExpression = $this->buildAttributeCodeExpression($eavAttributes); + + $selects[] = $connection->select() + ->from(['e' => $entityTableName], $entityTableAttributes) + ->joinLeft( + ['eav' => $this->resourceConnection->getTableName($attributeTable)], + \sprintf('e.%1$s = eav.%1$s', $linkField) . + $connection->quoteInto(' AND eav.attribute_id IN (?)', \array_keys($eavAttributesMetaData)) . + $connection->quoteInto(' AND eav.store_id = ?', \Magento\Store\Model\Store::DEFAULT_STORE_ID), + [] + ) + ->joinLeft( + ['store_eav' => $this->resourceConnection->getTableName($attributeTable)], + \sprintf( + 'e.%1$s = store_eav.%1$s AND store_eav.attribute_id = ' . + 'eav.attribute_id and store_eav.store_id = %2$d', + $linkField, + $storeId + ), + [] + ) + ->where('e.entity_id IN (?)', $entityIds) + ->columns( + [ + 'attribute_code' => $attributeCodeExpression, + 'value' => $attributeValueExpression + ] + ); + } + + return $connection->select()->union($selects, Select::SQL_UNION_ALL); + } + + /** + * Build expression for attribute code field. + * + * An example: + * + * ``` + * CASE + * WHEN eav.attribute_id = '73' THEN 'name' + * WHEN eav.attribute_id = '121' THEN 'url_key' + * END + * ``` + * + * @param array $eavAttributes + * @return \Zend_Db_Expr + */ + private function buildAttributeCodeExpression(array $eavAttributes): \Zend_Db_Expr + { + $dbConnection = $this->resourceConnection->getConnection(); + $expressionParts = ['CASE']; + + foreach ($eavAttributes as $attribute) { + $expressionParts[]= + $dbConnection->quoteInto('WHEN eav.attribute_id = ?', $attribute['attribute_id'], \Zend_Db::INT_TYPE) . + $dbConnection->quoteInto(' THEN ?', $attribute['attribute_code'], 'string'); + } + + $expressionParts[]= 'END'; + + return new \Zend_Db_Expr(implode(' ', $expressionParts)); + } + + /** + * Get list of attribute tables. + * + * Returns result in the following format: * + * ``` + * $attributeAttributeCodeTables = [ + * 'm2_catalog_product_entity_varchar' => + * '45' => [ + * 'attribute_id' => 45, + * 'backend_type' => 'varchar', + * 'name' => attribute_code, + * ] + * ] + * ]; + * ``` + * + * @param string $entityTable + * @param array $eavAttributesMetaData + * @return array + */ + private function getAttributeCodeTables($entityTable, $eavAttributesMetaData): array + { + $attributeAttributeCodeTables = []; + $metaTypes = \array_unique(\array_column($eavAttributesMetaData, 'backend_type')); + + foreach ($metaTypes as $type) { + if (\in_array($type, self::SUPPORTED_BACKEND_TYPES, true)) { + $tableName = \sprintf('%s_%s', $entityTable, $type); + $attributeAttributeCodeTables[$tableName] = array_filter( + $eavAttributesMetaData, + function ($attribute) use ($type) { + return $attribute['backend_type'] === $type; + } + ); + } + } + + return $attributeAttributeCodeTables; + } + + /** + * Get EAV attribute codes + * Remove attributes from entity table and attributes from exclude list + * Add linked attributes to output + * + * @param array $attributes + * @param array $entityTableAttributes + * @return array + */ + private function getEavAttributeCodes($attributes, $entityTableAttributes): array + { + $attributes = \array_diff($attributes, $entityTableAttributes); + $unusedAttributeList = []; + $newAttributes = []; + foreach ($this->linkedAttributes as $attribute => $linkedAttributes) { + if (null === $linkedAttributes) { + $unusedAttributeList[] = $attribute; + } elseif (\is_array($linkedAttributes) && \in_array($attribute, $attributes, true)) { + $newAttributes[] = $linkedAttributes; + } + } + $attributes = \array_diff($attributes, $unusedAttributeList); + + return \array_unique(\array_merge($attributes, ...$newAttributes)); + } + + /** + * Retrieve entity type id + * + * @return int + * @throws \Exception + */ + private function getEntityTypeId(): int + { + if (!isset($this->entityTypeIdMap[$this->entityType])) { + $this->entityTypeIdMap[$this->entityType] = (int)$this->eavConfig->getEntityType( + $this->metadataPool->getMetadata($this->entityType)->getEavEntityType() + )->getId(); + } + + return $this->entityTypeIdMap[$this->entityType]; + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Category/Query/CategoryAttributeQuery.php b/app/code/Magento/CatalogGraphQl/DataProvider/Category/Query/CategoryAttributeQuery.php new file mode 100644 index 0000000000000..0b796c1457254 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Category/Query/CategoryAttributeQuery.php @@ -0,0 +1,58 @@ +attributeQueryFactory = $attributeQueryFactory; + } + + /** + * Form and return query to get eav attributes for given categories + * + * @param array $categoryIds + * @param array $categoryAttributes + * @param int $storeId + * @return Select + * @throws \Zend_Db_Select_Exception + */ + public function getQuery(array $categoryIds, array $categoryAttributes, int $storeId): Select + { + $categoryAttributes = \array_merge($categoryAttributes, self::$requiredAttributes); + + $attributeQuery = $this->attributeQueryFactory->create([ + 'entityType' => CategoryInterface::class + ]); + + return $attributeQuery->getQuery($categoryIds, $categoryAttributes, $storeId); + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/CategoryAttributesMapper.php b/app/code/Magento/CatalogGraphQl/DataProvider/CategoryAttributesMapper.php new file mode 100644 index 0000000000000..1f8aa38d5b939 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/CategoryAttributesMapper.php @@ -0,0 +1,114 @@ +graphqlConfig = $graphqlConfig; + } + + /** + * Returns attribute values for given attribute codes. + * + * @param array $fetchResult + * @return array + */ + public function getAttributesValues(array $fetchResult): array + { + $attributes = []; + + foreach ($fetchResult as $row) { + if (!isset($attributes[$row['entity_id']])) { + $attributes[$row['entity_id']] = $row; + //TODO: do we need to introduce field mapping? + $attributes[$row['entity_id']]['id'] = $row['entity_id']; + } + if (isset($row['attribute_code'])) { + $attributes[$row['entity_id']][$row['attribute_code']] = $row['value']; + } + } + + return $this->formatAttributes($attributes); + } + + /** + * Format attributes that should be converted to array type + * + * @param array $attributes + * @return array + */ + private function formatAttributes(array $attributes): array + { + $arrayTypeAttributes = $this->getFieldsOfArrayType(); + + return $arrayTypeAttributes + ? array_map(function ($data) use ($arrayTypeAttributes) { + foreach ($arrayTypeAttributes as $attributeCode) { + $data[$attributeCode] = $this->valueToArray($data[$attributeCode] ?? null); + } + return $data; + }, $attributes) + : $attributes; + } + + /** + * Cast string to array + * + * @param string|null $value + * @return array + */ + private function valueToArray($value): array + { + return $value ? \explode(',', $value) : []; + } + + /** + * Get fields that should be converted to array type + * + * @return array + */ + private function getFieldsOfArrayType(): array + { + $categoryTreeSchema = $this->graphqlConfig->getConfigElement('CategoryTree'); + if (!$categoryTreeSchema instanceof Type) { + throw new \LogicException('CategoryTree type not defined in schema.'); + } + + $fields = []; + foreach ($categoryTreeSchema->getInterfaces() as $interface) { + /** @var InterfaceType $configElement */ + $configElement = $this->graphqlConfig->getConfigElement($interface['interface']); + + foreach ($configElement->getFields() as $field) { + if ($field->isList()) { + $fields[] = $field->getName(); + } + } + } + + return $fields; + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php new file mode 100644 index 0000000000000..7781473128754 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php @@ -0,0 +1,107 @@ + [ + * attribute_code => code, + * attribute_label => attribute label, + * option_label => option label, + * options => [option_id => 'option label', ...], + * ] + * ... + * ] + */ +class AttributeOptionProvider +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ResourceConnection $resourceConnection + */ + public function __construct(ResourceConnection $resourceConnection) + { + $this->resourceConnection = $resourceConnection; + } + + /** + * Get option data. Return list of attributes with option data + * + * @param array $optionIds + * @return array + * @throws \Zend_Db_Statement_Exception + */ + public function getOptions(array $optionIds): array + { + if (!$optionIds) { + return []; + } + + $connection = $this->resourceConnection->getConnection(); + $select = $connection->select() + ->from( + ['a' => $this->resourceConnection->getTableName('eav_attribute')], + [ + 'attribute_id' => 'a.attribute_id', + 'attribute_code' => 'a.attribute_code', + 'attribute_label' => 'a.frontend_label', + ] + ) + ->joinInner( + ['options' => $this->resourceConnection->getTableName('eav_attribute_option')], + 'a.attribute_id = options.attribute_id', + [] + ) + ->joinInner( + ['option_value' => $this->resourceConnection->getTableName('eav_attribute_option_value')], + 'options.option_id = option_value.option_id', + [ + 'option_label' => 'option_value.value', + 'option_id' => 'option_value.option_id', + ] + ) + ->where('option_value.option_id IN (?)', $optionIds); + + return $this->formatResult($select); + } + + /** + * Format result + * + * @param \Magento\Framework\DB\Select $select + * @return array + * @throws \Zend_Db_Statement_Exception + */ + private function formatResult(\Magento\Framework\DB\Select $select): array + { + $statement = $this->resourceConnection->getConnection()->query($select); + + $result = []; + while ($option = $statement->fetch()) { + if (!isset($result[$option['attribute_code']])) { + $result[$option['attribute_code']] = [ + 'attribute_id' => $option['attribute_id'], + 'attribute_code' => $option['attribute_code'], + 'attribute_label' => $option['attribute_label'], + 'options' => [], + ]; + } + $result[$option['attribute_code']]['options'][$option['option_id']] = $option['option_label']; + } + + return $result; + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php new file mode 100644 index 0000000000000..82d167e323fc8 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php @@ -0,0 +1,179 @@ +attributeOptionProvider = $attributeOptionProvider; + $this->bucketNameFilter = \array_merge($this->bucketNameFilter, $bucketNameFilter); + } + + /** + * @inheritdoc + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws \Zend_Db_Statement_Exception + */ + public function build(AggregationInterface $aggregation, ?int $storeId): array + { + $attributeOptions = $this->getAttributeOptions($aggregation); + + // build layer per attribute + $result = []; + foreach ($this->getAttributeBuckets($aggregation) as $bucket) { + $bucketName = $bucket->getName(); + $attributeCode = \preg_replace('~_bucket$~', '', $bucketName); + $attribute = $attributeOptions[$attributeCode] ?? []; + + $result[$bucketName] = $this->buildLayer( + $attribute['attribute_label'] ?? $bucketName, + \count($bucket->getValues()), + $attribute['attribute_code'] ?? $bucketName + ); + + foreach ($bucket->getValues() as $value) { + $metrics = $value->getMetrics(); + $result[$bucketName]['filter_items'][] = $this->buildItem( + $attribute['options'][$metrics['value']] ?? $metrics['value'], + $metrics['value'], + $metrics['count'] + ); + } + } + + return $result; + } + + /** + * Get attribute buckets excluding specified bucket names + * + * @param AggregationInterface $aggregation + * @return \Generator|BucketInterface[] + */ + private function getAttributeBuckets(AggregationInterface $aggregation) + { + foreach ($aggregation->getBuckets() as $bucket) { + if (\in_array($bucket->getName(), $this->bucketNameFilter, true)) { + continue; + } + if ($this->isBucketEmpty($bucket)) { + continue; + } + yield $bucket; + } + } + + /** + * Format layer data + * + * @param string $layerName + * @param string $itemsCount + * @param string $requestName + * @return array + */ + private function buildLayer($layerName, $itemsCount, $requestName): array + { + return [ + 'name' => $layerName, + 'filter_items_count' => $itemsCount, + 'request_var' => $requestName + ]; + } + + /** + * Format layer item data + * + * @param string $label + * @param string|int $value + * @param string|int $count + * @return array + */ + private function buildItem($label, $value, $count): array + { + return [ + 'label' => $label, + 'value_string' => $value, + 'items_count' => $count, + ]; + } + + /** + * Check that bucket contains data + * + * @param BucketInterface|null $bucket + * @return bool + */ + private function isBucketEmpty(?BucketInterface $bucket): bool + { + return null === $bucket || !$bucket->getValues(); + } + + /** + * Get list of attributes with options + * + * @param AggregationInterface $aggregation + * @return array + * @throws \Zend_Db_Statement_Exception + */ + private function getAttributeOptions(AggregationInterface $aggregation): array + { + $attributeOptionIds = []; + foreach ($this->getAttributeBuckets($aggregation) as $bucket) { + $attributeOptionIds[] = \array_map(function (AggregationValueInterface $value) { + return $value->getValue(); + }, $bucket->getValues()); + } + + if (!$attributeOptionIds) { + return []; + } + + return $this->attributeOptionProvider->getOptions(\array_merge(...$attributeOptionIds)); + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php new file mode 100644 index 0000000000000..c726f66e8c926 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php @@ -0,0 +1,173 @@ + [ + 'request_name' => 'category_id', + 'label' => 'Category' + ], + ]; + + /** + * @var CategoryAttributeQuery + */ + private $categoryAttributeQuery; + + /** + * @var CategoryAttributesMapper + */ + private $attributesMapper; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var RootCategoryProvider + */ + private $rootCategoryProvider; + + /** + * @param CategoryAttributeQuery $categoryAttributeQuery + * @param CategoryAttributesMapper $attributesMapper + * @param RootCategoryProvider $rootCategoryProvider + * @param ResourceConnection $resourceConnection + */ + public function __construct( + CategoryAttributeQuery $categoryAttributeQuery, + CategoryAttributesMapper $attributesMapper, + RootCategoryProvider $rootCategoryProvider, + ResourceConnection $resourceConnection + ) { + $this->categoryAttributeQuery = $categoryAttributeQuery; + $this->attributesMapper = $attributesMapper; + $this->resourceConnection = $resourceConnection; + $this->rootCategoryProvider = $rootCategoryProvider; + } + + /** + * @inheritdoc + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Zend_Db_Select_Exception + */ + public function build(AggregationInterface $aggregation, ?int $storeId): array + { + $bucket = $aggregation->getBucket(self::CATEGORY_BUCKET); + if ($this->isBucketEmpty($bucket)) { + return []; + } + + $categoryIds = \array_map(function (AggregationValueInterface $value) { + return (int)$value->getValue(); + }, $bucket->getValues()); + + $categoryIds = \array_diff($categoryIds, [$this->rootCategoryProvider->getRootCategory($storeId)]); + $categoryLabels = \array_column( + $this->attributesMapper->getAttributesValues( + $this->resourceConnection->getConnection()->fetchAll( + $this->categoryAttributeQuery->getQuery($categoryIds, ['name'], $storeId) + ) + ), + 'name', + 'entity_id' + ); + + if (!$categoryLabels) { + return []; + } + + $result = $this->buildLayer( + self::$bucketMap[self::CATEGORY_BUCKET]['label'], + \count($categoryIds), + self::$bucketMap[self::CATEGORY_BUCKET]['request_name'] + ); + + foreach ($bucket->getValues() as $value) { + $categoryId = $value->getValue(); + if (!\in_array($categoryId, $categoryIds, true)) { + continue ; + } + $result['filter_items'][] = $this->buildItem( + $categoryLabels[$categoryId] ?? $categoryId, + $categoryId, + $value->getMetrics()['count'] + ); + } + + return [$result]; + } + + /** + * Format layer data + * + * @param string $layerName + * @param string $itemsCount + * @param string $requestName + * @return array + */ + private function buildLayer($layerName, $itemsCount, $requestName): array + { + return [ + 'name' => $layerName, + 'filter_items_count' => $itemsCount, + 'request_var' => $requestName + ]; + } + + /** + * Format layer item data + * + * @param string $label + * @param string|int $value + * @param string|int $count + * @return array + */ + private function buildItem($label, $value, $count): array + { + return [ + 'label' => $label, + 'value_string' => $value, + 'items_count' => $count, + ]; + } + + /** + * Check that bucket contains data + * + * @param BucketInterface|null $bucket + * @return bool + */ + private function isBucketEmpty(?BucketInterface $bucket): bool + { + return null === $bucket || !$bucket->getValues(); + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php new file mode 100644 index 0000000000000..77f44afb4f679 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php @@ -0,0 +1,107 @@ + [ + 'request_name' => 'price', + 'label' => 'Price' + ], + ]; + + /** + * @inheritdoc + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function build(AggregationInterface $aggregation, ?int $storeId): array + { + $bucket = $aggregation->getBucket(self::PRICE_BUCKET); + if ($this->isBucketEmpty($bucket)) { + return []; + } + + $result = $this->buildLayer( + self::$bucketMap[self::PRICE_BUCKET]['label'], + \count($bucket->getValues()), + self::$bucketMap[self::PRICE_BUCKET]['request_name'] + ); + + foreach ($bucket->getValues() as $value) { + $metrics = $value->getMetrics(); + $result['filter_items'][] = $this->buildItem( + \str_replace('_', '-', $metrics['value']), + $metrics['value'], + $metrics['count'] + ); + } + + return [$result]; + } + + /** + * Format layer data + * + * @param string $layerName + * @param string $itemsCount + * @param string $requestName + * @return array + */ + private function buildLayer($layerName, $itemsCount, $requestName): array + { + return [ + 'name' => $layerName, + 'filter_items_count' => $itemsCount, + 'request_var' => $requestName + ]; + } + + /** + * Format layer item data + * + * @param string $label + * @param string|int $value + * @param string|int $count + * @return array + */ + private function buildItem($label, $value, $count): array + { + return [ + 'label' => $label, + 'value_string' => $value, + 'items_count' => $count, + ]; + } + + /** + * Check that bucket contains data + * + * @param BucketInterface|null $bucket + * @return bool + */ + private function isBucketEmpty(?BucketInterface $bucket): bool + { + return null === $bucket || !$bucket->getValues(); + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php new file mode 100644 index 0000000000000..ff661236be62f --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php @@ -0,0 +1,43 @@ +builders = $builders; + } + + /** + * @inheritdoc + */ + public function build(AggregationInterface $aggregation, ?int $storeId): array + { + $layers = []; + foreach ($this->builders as $builder) { + $layers[] = $builder->build($aggregation, $storeId); + } + $layers = \array_merge(...$layers); + + return \array_filter($layers); + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilderInterface.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilderInterface.php new file mode 100644 index 0000000000000..bd55bc6938b39 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilderInterface.php @@ -0,0 +1,40 @@ + 'layer name', + * 'filter_items_count' => 'filter items count', + * 'request_var' => 'filter name in request', + * 'filter_items' => [ + * 'label' => 'item name', + * 'value_string' => 'item value, e.g. category ID', + * 'items_count' => 'product count', + * ], + * ], + * ... + * ]; + */ +interface LayerBuilderInterface +{ + /** + * Build layer data + * + * @param AggregationInterface $aggregation + * @param int|null $storeId + * @return array [[{layer data}], ...] + */ + public function build(AggregationInterface $aggregation, ?int $storeId): array; +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/RootCategoryProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/RootCategoryProvider.php new file mode 100644 index 0000000000000..4b8a4a31b3c35 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/RootCategoryProvider.php @@ -0,0 +1,55 @@ +resourceConnection = $resourceConnection; + } + + /** + * Get root category for specified store id + * + * @param int $storeId + * @return int + */ + public function getRootCategory(int $storeId): int + { + $connection = $this->resourceConnection->getConnection(); + + $select = $connection->select() + ->from( + ['store' => $this->resourceConnection->getTableName('store')], + [] + ) + ->join( + ['store_group' => $this->resourceConnection->getTableName('store_group')], + 'store.group_id = store_group.group_id', + ['root_category_id' => 'store_group.root_category_id'] + ) + ->where('store.store_id = ?', $storeId); + + return (int)$connection->fetchOne($select); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php index d493c7ba8e66f..9aa632b4fafbb 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php @@ -10,7 +10,8 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -//use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\LayerBuilder; +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\LayerBuilder; +use Magento\Store\Api\Data\StoreInterface; /** * Layered navigation filters resolver, used for GraphQL request processing. @@ -22,24 +23,25 @@ class LayerFilters implements ResolverInterface */ private $filtersDataProvider; -// /** -// * @var LayerBuilder -// */ -// private $layerBuilder; + /** + * @var LayerBuilder + */ + private $layerBuilder; /** * @param \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider * @param \Magento\Framework\Registry $registry + * @param LayerBuilder $layerBuilder */ public function __construct( \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider, - \Magento\Framework\Registry $registry - //LayerBuilder $layerBuilder + \Magento\Framework\Registry $registry, + LayerBuilder $layerBuilder ) { $this->filtersDataProvider = $filtersDataProvider; $this->registry = $registry; - //$this->layerBuilder = $layerBuilder; + $this->layerBuilder = $layerBuilder; } /** @@ -56,7 +58,9 @@ public function resolve( return null; } $aggregations = $this->registry->registry('aggregations'); - return []; - //return $this->layerBuilder->build($aggregations, 1); + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + $storeId = (int)$store->getId(); + return $this->layerBuilder->build($aggregations, $storeId); } } diff --git a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml index 8fe3d19619fc3..3ada365ec5065 100644 --- a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml @@ -101,4 +101,14 @@ + + + + + Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Price + Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Category + Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Attribute + + + From 2837f1e985f1168e3630dc25c90c36cf1f132dec Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Fri, 9 Aug 2019 11:50:57 -0500 Subject: [PATCH 004/147] MC-18988: Investigate layer navigation code in lightweight resolver implementation --- app/code/Magento/CatalogGraphQl/etc/schema.graphqls | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 0b71b849d09a4..1ce46cf3cbef9 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -315,11 +315,6 @@ input ProductFilterInput @doc(description: "ProductFilterInput defines the filte country_of_manufacture: FilterTypeInput @doc(description: "The product's country of origin.") custom_layout: FilterTypeInput @doc(description: "The name of a custom layout.") gift_message_available: FilterTypeInput @doc(description: "Indicates whether a gift message is available.") - cat: FilterTypeInput @doc(description: "CATEGORY attribute name") - mysize: FilterTypeInput @doc(description: "size attribute name") - mycolor: FilterTypeInput @doc(description: "color attribute name") - ca_1_631447041: FilterTypeInput @doc(description: "CATEGORY attribute name") - attributeset2attribute1: FilterTypeInput @doc(description: "CATEGORY attribute name") visibility: FilterTypeInput @doc(description: "CATEGORY attribute name") price_dynamic_algorithm: FilterTypeInput @doc(description: "CATEGORY attribute name") category_ids: FilterTypeInput @doc(description: "CATEGORY attribute name") From e6083f28870d87f676901e3a9d1739defe1bc0c4 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Mon, 12 Aug 2019 11:29:05 -0500 Subject: [PATCH 005/147] MC-19103: Get aggregation from search and expose them to the filters resolver or field --- .../Model/Resolver/LayerFilters.php | 22 +++++----- .../Model/Resolver/Products.php | 2 +- .../Model/Resolver/Products/Query/Filter.php | 41 ++++++++++++++----- .../Model/Resolver/Products/Query/Search.php | 21 ++++++---- .../Model/Resolver/Products/SearchResult.php | 34 +++++++-------- .../Resolver/Products/SearchResultFactory.php | 10 ++--- 6 files changed, 79 insertions(+), 51 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php index 9aa632b4fafbb..6ef4e72627e82 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php @@ -30,17 +30,13 @@ class LayerFilters implements ResolverInterface /** * @param \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider - * @param \Magento\Framework\Registry $registry * @param LayerBuilder $layerBuilder */ public function __construct( \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider, - \Magento\Framework\Registry $registry, LayerBuilder $layerBuilder - ) { $this->filtersDataProvider = $filtersDataProvider; - $this->registry = $registry; $this->layerBuilder = $layerBuilder; } @@ -54,13 +50,19 @@ public function resolve( array $value = null, array $args = null ) { - if (!isset($value['layer_type'])) { + if (!isset($value['layer_type']) || !isset($value['search_result'])) { return null; } - $aggregations = $this->registry->registry('aggregations'); - /** @var StoreInterface $store */ - $store = $context->getExtensionAttributes()->getStore(); - $storeId = (int)$store->getId(); - return $this->layerBuilder->build($aggregations, $storeId); + + $aggregations = $value['search_result']->getSearchAggregation(); + + if ($aggregations) { + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + $storeId = (int)$store->getId(); + return $this->layerBuilder->build($aggregations, $storeId); + } else { + return []; + } } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php index 93ecf9c881548..9eca99b486df4 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php @@ -117,7 +117,7 @@ public function resolve( 'current_page' => $currentPage, 'total_pages' => $maxPages ], - //'filters' => $aggregations + 'search_result' => $searchResult, 'layer_type' => $layerType ]; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php index 62e2f0c488c6c..6771777341281 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php @@ -7,6 +7,7 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\Query; +use GraphQL\Language\AST\SelectionNode; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product; @@ -79,7 +80,12 @@ public function getResult( $productArray[$product->getId()]['model'] = $product; } - return $this->searchResultFactory->create($products->getTotalCount(), $productArray); + return $this->searchResultFactory->create( + [ + 'totalCount' => $products->getTotalCount(), + 'productsSearchResult' => $productArray + ] + ); } /** @@ -99,20 +105,35 @@ private function getProductFields(ResolveInfo $info) : array if ($selection->name->value !== 'items') { continue; } + $fieldNames[] = $this->collectProductFieldNames($selection, $fieldNames); + } + } + + $fieldNames = array_merge(...$fieldNames); + + return $fieldNames; + } - foreach ($selection->selectionSet->selections as $itemSelection) { - if ($itemSelection->kind === 'InlineFragment') { - foreach ($itemSelection->selectionSet->selections as $inlineSelection) { - if ($inlineSelection->kind === 'InlineFragment') { - continue; - } - $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value); - } + /** + * Collect field names for each node in selection + * + * @param SelectionNode $selection + * @param array $fieldNames + * @return array + */ + private function collectProductFieldNames(SelectionNode $selection, array $fieldNames = []): array + { + foreach ($selection->selectionSet->selections as $itemSelection) { + if ($itemSelection->kind === 'InlineFragment') { + foreach ($itemSelection->selectionSet->selections as $inlineSelection) { + if ($inlineSelection->kind === 'InlineFragment') { continue; } - $fieldNames[] = $this->fieldTranslator->translate($itemSelection->name->value); + $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value); } + continue; } + $fieldNames[] = $this->fieldTranslator->translate($itemSelection->name->value); } return $fieldNames; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index 7a766269af602..1fdd64f7bbc68 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -63,7 +63,6 @@ class Search * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool * @param \Magento\Search\Model\Search\PageSizeProvider $pageSize * @param SearchCriteriaInterfaceFactory $searchCriteriaFactory - * @param \Magento\Framework\Registry $registry */ public function __construct( SearchInterface $search, @@ -72,8 +71,7 @@ public function __construct( SearchResultFactory $searchResultFactory, \Magento\Framework\EntityManager\MetadataPool $metadataPool, \Magento\Search\Model\Search\PageSizeProvider $pageSize, - SearchCriteriaInterfaceFactory $searchCriteriaFactory, - \Magento\Framework\Registry $registry + SearchCriteriaInterfaceFactory $searchCriteriaFactory ) { $this->search = $search; $this->filterHelper = $filterHelper; @@ -82,7 +80,6 @@ public function __construct( $this->metadataPool = $metadataPool; $this->pageSizeProvider = $pageSize; $this->searchCriteriaFactory = $searchCriteriaFactory; - $this->registry = $registry; } /** @@ -98,14 +95,16 @@ public function getResult(SearchCriteriaInterface $searchCriteria, ResolveInfo $ $idField = $this->metadataPool->getMetadata( \Magento\Catalog\Api\Data\ProductInterface::class )->getIdentifierField(); + $realPageSize = $searchCriteria->getPageSize(); $realCurrentPage = $searchCriteria->getCurrentPage(); // Current page must be set to 0 and page size to max for search to grab all ID's as temporary workaround - //$pageSize = $this->pageSizeProvider->getMaxPageSize(); - $searchCriteria->setPageSize(10000); + $pageSize = $this->pageSizeProvider->getMaxPageSize(); + $searchCriteria->setPageSize($pageSize); $searchCriteria->setCurrentPage(0); $itemsResults = $this->search->search($searchCriteria); - $this->registry->register('aggregations', $itemsResults->getAggregations()); + $aggregation = $itemsResults->getAggregations(); + $ids = []; $searchIds = []; foreach ($itemsResults->getItems() as $item) { @@ -139,7 +138,13 @@ public function getResult(SearchCriteriaInterface $searchCriteria, ResolveInfo $ } } - return $this->searchResultFactory->create($searchResult->getTotalCount(), $products); + return $this->searchResultFactory->create( + [ + 'totalCount' => $searchResult->getTotalCount(), + 'productsSearchResult' => $products, + 'searchAggregation' => $aggregation + ] + ); } /** diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php index 6e229bdc38a31..849d9065d2449 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php @@ -7,31 +7,21 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products; -use Magento\Framework\Api\SearchResultsInterface; +use Magento\Framework\Api\Search\AggregationInterface; /** * Container for a product search holding the item result and the array in the GraphQL-readable product type format. */ class SearchResult { - /** - * @var SearchResultsInterface - */ - private $totalCount; - - /** - * @var array - */ - private $productsSearchResult; + private $data; /** - * @param int $totalCount - * @param array $productsSearchResult + * @param array $data */ - public function __construct(int $totalCount, array $productsSearchResult) + public function __construct(array $data) { - $this->totalCount = $totalCount; - $this->productsSearchResult = $productsSearchResult; + $this->data = $data; } /** @@ -41,7 +31,7 @@ public function __construct(int $totalCount, array $productsSearchResult) */ public function getTotalCount() : int { - return $this->totalCount; + return $this->data['totalCount'] ?? 0; } /** @@ -51,6 +41,16 @@ public function getTotalCount() : int */ public function getProductsSearchResult() : array { - return $this->productsSearchResult; + return $this->data['productsSearchResult'] ?? []; + } + + /** + * Retrieve aggregated search results + * + * @return AggregationInterface|null + */ + public function getSearchAggregation(): ?AggregationInterface + { + return $this->data['searchAggregation'] ?? null; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResultFactory.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResultFactory.php index aec9362f47c3a..479e6a3f96235 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResultFactory.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResultFactory.php @@ -30,15 +30,15 @@ public function __construct(ObjectManagerInterface $objectManager) /** * Instantiate SearchResult * - * @param int $totalCount - * @param array $productsSearchResult + * @param array $data * @return SearchResult */ - public function create(int $totalCount, array $productsSearchResult) : SearchResult - { + public function create( + array $data + ): SearchResult { return $this->objectManager->create( SearchResult::class, - ['totalCount' => $totalCount, 'productsSearchResult' => $productsSearchResult] + ['data' => $data] ); } } From 0fcfffab4978b3b8e3c43f2e233442a98607e080 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Mon, 12 Aug 2019 18:19:35 -0500 Subject: [PATCH 006/147] MC-19102: Adding new search request and use search retrieve results - use filter to paginate --- .../Product/SearchCriteriaBuilder.php | 152 ++++++++++++++++++ .../Model/Resolver/Products/Query/Search.php | 50 +----- 2 files changed, 156 insertions(+), 46 deletions(-) create mode 100644 app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php new file mode 100644 index 0000000000000..f49db706e3aaf --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -0,0 +1,152 @@ +scopeConfig = $scopeConfig; + $this->filterBuilder = $filterBuilder; + $this->filterGroupBuilder = $filterGroupBuilder; + $this->builder = $builder; + $this->visibility = $visibility; + } + + /** + * Build search criteria + * + * @param array $args + * @param bool $includeAggregation + * @return SearchCriteriaInterface + */ + public function build(array $args, bool $includeAggregation): SearchCriteriaInterface + { + $searchCriteria = $this->builder->build('products', $args); + $searchCriteria->setRequestName('catalog_view_container'); + if ($includeAggregation) { + $this->preparePriceAggregation($searchCriteria); + } + + if (!empty($args['search'])) { + $this->addFilter($searchCriteria, 'search_term', $args['search']); + if (!$searchCriteria->getSortOrders()) { + $searchCriteria->setSortOrders(['_score' => \Magento\Framework\Api\SortOrder::SORT_DESC]); + } + } + + $this->addVisibilityFilter($searchCriteria, !empty($args['search']), !empty($args['filter'])); + + $searchCriteria->setCurrentPage($args['currentPage']); + $searchCriteria->setPageSize($args['pageSize']); + + return $searchCriteria; + } + + /** + * Add filter by visibility + * + * @param SearchCriteriaInterface $searchCriteria + * @param bool $isSearch + * @param bool $isFilter + */ + private function addVisibilityFilter(SearchCriteriaInterface $searchCriteria, bool $isSearch, bool $isFilter): void + { + if ($isFilter && $isSearch) { + // Index already contains products filtered by visibility: catalog, search, both + return ; + } + $visibilityIds = $isSearch + ? $this->visibility->getVisibleInSearchIds() + : $this->visibility->getVisibleInCatalogIds(); + + $this->addFilter($searchCriteria, 'visibility', $visibilityIds); + } + + /** + * Prepare price aggregation algorithm + * + * @param SearchCriteriaInterface $searchCriteria + * @return void + */ + private function preparePriceAggregation(SearchCriteriaInterface $searchCriteria): void + { + $priceRangeCalculation = $this->scopeConfig->getValue( + \Magento\Catalog\Model\Layer\Filter\Dynamic\AlgorithmFactory::XML_PATH_RANGE_CALCULATION, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + if ($priceRangeCalculation) { + $this->addFilter($searchCriteria, 'price_dynamic_algorithm', $priceRangeCalculation); + } + } + + /** + * Add filter to search criteria + * + * @param SearchCriteriaInterface $searchCriteria + * @param string $field + * @param mixed $value + */ + private function addFilter(SearchCriteriaInterface $searchCriteria, string $field, $value): void + { + $filter = $this->filterBuilder + ->setField($field) + ->setValue($value) + ->create(); + $this->filterGroupBuilder->addFilter($filter); + $filterGroups = $searchCriteria->getFilterGroups(); + $filterGroups[] = $this->filterGroupBuilder->create(); + $searchCriteria->setFilterGroups($filterGroups); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index 1fdd64f7bbc68..11fb8b79f241e 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -115,60 +115,18 @@ public function getResult(SearchCriteriaInterface $searchCriteria, ResolveInfo $ $searchCriteriaIds = $this->searchCriteriaFactory->create(); $filter = $this->filterHelper->generate($idField, 'in', $searchIds); $searchCriteriaIds = $this->filterHelper->add($searchCriteriaIds, $filter); - $searchResult = $this->filterQuery->getResult($searchCriteriaIds, $info, true); - + $searchCriteriaIds->setSortOrders($searchCriteria->getSortOrders()); $searchCriteriaIds->setPageSize($realPageSize); $searchCriteriaIds->setCurrentPage($realCurrentPage); - $paginatedProducts = $this->paginateList($searchResult, $searchCriteriaIds); - - $products = []; - if (!isset($searchCriteria->getSortOrders()[0])) { - foreach ($paginatedProducts as $product) { - if (in_array($product[$idField], $searchIds)) { - $ids[$product[$idField]] = $product; - } - } - $products = array_filter($ids); - } else { - foreach ($paginatedProducts as $product) { - $productId = isset($product['entity_id']) ? $product['entity_id'] : $product[$idField]; - if (in_array($productId, $searchIds)) { - $products[] = $product; - } - } - } + + $searchResult = $this->filterQuery->getResult($searchCriteriaIds, $info, true); return $this->searchResultFactory->create( [ 'totalCount' => $searchResult->getTotalCount(), - 'productsSearchResult' => $products, + 'productsSearchResult' => $searchResult->getProductsSearchResult(), 'searchAggregation' => $aggregation ] ); } - - /** - * Paginate an array of Ids that get pulled back in search based off search criteria and total count. - * - * @param SearchResult $searchResult - * @param SearchCriteriaInterface $searchCriteria - * @return int[] - */ - private function paginateList(SearchResult $searchResult, SearchCriteriaInterface $searchCriteria) : array - { - $length = $searchCriteria->getPageSize(); - // Search starts pages from 0 - $offset = $length * ($searchCriteria->getCurrentPage() - 1); - - if ($searchCriteria->getPageSize()) { - $maxPages = ceil($searchResult->getTotalCount() / $searchCriteria->getPageSize()); - } else { - $maxPages = 0; - } - - if ($searchCriteria->getCurrentPage() > $maxPages && $searchResult->getTotalCount() > 0) { - $offset = (int)$maxPages; - } - return array_slice($searchResult->getProductsSearchResult(), $offset, $length); - } } From 5b0ef75ccbb1cd04adff985fbaa76cd342512de0 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Mon, 12 Aug 2019 21:32:30 -0500 Subject: [PATCH 007/147] MC-19102: Adding new search request and use search retrieve results - fix attribute list - fix category_ids - fix pagination - deprecation of searchCriteria to be used in the new builder --- .../Model/Resolver/Products.php | 59 +++++++++++-------- .../ProductEntityAttributesForAst.php | 37 +++++++++--- .../Model/Resolver/Products/Query/Search.php | 13 +++- .../Model/Resolver/Products/SearchResult.php | 30 ++++++++++ .../CatalogGraphQl/etc/schema.graphqls | 3 - .../Query/Resolver/Argument/AstConverter.php | 11 +--- 6 files changed, 107 insertions(+), 46 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php index 9eca99b486df4..10d0d10747eaf 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php @@ -16,6 +16,7 @@ use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\SearchFilter; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Catalog\Model\Layer\Resolver; +use Magento\CatalogGraphQl\DataProvider\Product\SearchCriteriaBuilder; /** * Products field resolver, used for GraphQL request processing. @@ -24,6 +25,7 @@ class Products implements ResolverInterface { /** * @var Builder + * @deprecated */ private $searchCriteriaBuilder; @@ -34,30 +36,41 @@ class Products implements ResolverInterface /** * @var Filter + * @deprecated */ private $filterQuery; /** * @var SearchFilter + * @deprecated */ private $searchFilter; + /** + * @var SearchCriteriaBuilder + */ + private $searchApiCriteriaBuilder; + /** * @param Builder $searchCriteriaBuilder * @param Search $searchQuery * @param Filter $filterQuery * @param SearchFilter $searchFilter + * @param SearchCriteriaBuilder|null $searchCriteriaBuilder */ public function __construct( Builder $searchCriteriaBuilder, Search $searchQuery, Filter $filterQuery, - SearchFilter $searchFilter + SearchFilter $searchFilter, + SearchCriteriaBuilder $searchApiCriteriaBuilder = null ) { $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->searchQuery = $searchQuery; $this->filterQuery = $filterQuery; $this->searchFilter = $searchFilter; + $this->searchApiCriteriaBuilder = $searchApiCriteriaBuilder ?? + \Magento\Framework\App\ObjectManager::getInstance()->get(SearchCriteriaBuilder::class); } /** @@ -70,41 +83,37 @@ public function resolve( array $value = null, array $args = null ) { - $searchCriteria = $this->searchCriteriaBuilder->build($field->getName(), $args); if ($args['currentPage'] < 1) { throw new GraphQlInputException(__('currentPage value must be greater than 0.')); } if ($args['pageSize'] < 1) { throw new GraphQlInputException(__('pageSize value must be greater than 0.')); } - $searchCriteria->setCurrentPage($args['currentPage']); - $searchCriteria->setPageSize($args['pageSize']); if (!isset($args['search']) && !isset($args['filter'])) { throw new GraphQlInputException( __("'search' or 'filter' input argument is required.") ); - } elseif (isset($args['search'])) { - $layerType = Resolver::CATALOG_LAYER_SEARCH; - $this->searchFilter->add($args['search'], $searchCriteria); - $searchResult = $this->searchQuery->getResult($searchCriteria, $info); - } else { - $layerType = Resolver::CATALOG_LAYER_CATEGORY; - $searchCriteria->setRequestName('catalog_view_container'); - $searchResult = $this->searchQuery->getResult($searchCriteria, $info); - } - //possible division by 0 - if ($searchCriteria->getPageSize()) { - $maxPages = ceil($searchResult->getTotalCount() / $searchCriteria->getPageSize()); - } else { - $maxPages = 0; } - $currentPage = $searchCriteria->getCurrentPage(); - if ($searchCriteria->getCurrentPage() > $maxPages && $searchResult->getTotalCount() > 0) { + //get product children fields queried + $productFields = (array)$info->getFieldSelection(1); + + $searchCriteria = $this->searchApiCriteriaBuilder->build( + $args, + isset($productFields['filters']) + ); + + $searchCriteria->setCurrentPage($args['currentPage']); + $searchCriteria->setPageSize($args['pageSize']); + + + $searchResult = $this->searchQuery->getResult($searchCriteria, $info); + + if ($searchResult->getCurrentPage() > $searchResult->getTotalPages() && $searchResult->getTotalCount() > 0) { throw new GraphQlInputException( __( 'currentPage value %1 specified is greater than the %2 page(s) available.', - [$currentPage, $maxPages] + [$searchResult->getCurrentPage(), $searchResult->getTotalPages()] ) ); } @@ -113,12 +122,12 @@ public function resolve( 'total_count' => $searchResult->getTotalCount(), 'items' => $searchResult->getProductsSearchResult(), 'page_info' => [ - 'page_size' => $searchCriteria->getPageSize(), - 'current_page' => $currentPage, - 'total_pages' => $maxPages + 'page_size' => $searchResult->getPageSize(), + 'current_page' => $searchResult->getCurrentPage(), + 'total_pages' => $searchResult->getTotalPages() ], 'search_result' => $searchResult, - 'layer_type' => $layerType + 'layer_type' => isset($args['search']) ? Resolver::CATALOG_LAYER_SEARCH : Resolver::CATALOG_LAYER_CATEGORY, ]; return $data; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php index a547f63b217fe..17409210808cc 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php @@ -23,24 +23,41 @@ class ProductEntityAttributesForAst implements FieldEntityAttributesInterface private $config; /** + * Additional attributes that are not retrieved by getting fields from ProductInterface + * * @var array */ private $additionalAttributes = ['min_price', 'max_price', 'category_id']; + /** + * Array to translate graphql field to internal entity attribute + * + * @var array + */ + private $translatedAttributes = ['category_id' => 'category_ids']; + /** * @param ConfigInterface $config - * @param array $additionalAttributes + * @param string[] $additionalAttributes + * @param array $translatedAttributes */ public function __construct( ConfigInterface $config, - array $additionalAttributes = [] + array $additionalAttributes = [], + array $translatedAttributes = [] ) { $this->config = $config; $this->additionalAttributes = array_merge($this->additionalAttributes, $additionalAttributes); + $this->translatedAttributes = array_merge($this->translatedAttributes, $translatedAttributes); } /** - * {@inheritdoc} + * @inheritdoc + * + * Gather all the product entity attributes that can be filtered by search criteria. + * Example format ['attributeNameInGraphQl' => ['type' => 'String'. 'fieldName' => 'attributeNameInSearchCriteria']] + * + * @return array */ public function getEntityAttributes() : array { @@ -55,14 +72,20 @@ public function getEntityAttributes() : array $configElement = $this->config->getConfigElement($interface['interface']); foreach ($configElement->getFields() as $field) { - $fields[$field->getName()] = 'String'; + $fields[$field->getName()] = [ + 'type' => 'String', + 'fieldName' => $this->translatedAttributes[$field->getName()] ?? $field->getName(), + ]; } } - foreach ($this->additionalAttributes as $attribute) { - $fields[$attribute] = 'String'; + foreach ($this->additionalAttributes as $attributeName) { + $fields[$attributeName] = [ + 'type' => 'String', + 'fieldName' => $this->translatedAttributes[$attributeName] ?? $attributeName, + ]; } - return array_keys($fields); + return $fields; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index 11fb8b79f241e..daa3619d17b83 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -103,7 +103,6 @@ public function getResult(SearchCriteriaInterface $searchCriteria, ResolveInfo $ $searchCriteria->setPageSize($pageSize); $searchCriteria->setCurrentPage(0); $itemsResults = $this->search->search($searchCriteria); - $aggregation = $itemsResults->getAggregations(); $ids = []; $searchIds = []; @@ -121,11 +120,21 @@ public function getResult(SearchCriteriaInterface $searchCriteria, ResolveInfo $ $searchResult = $this->filterQuery->getResult($searchCriteriaIds, $info, true); + //possible division by 0 + if ($realPageSize) { + $maxPages = (int)ceil($searchResult->getTotalCount() / $realPageSize); + } else { + $maxPages = 0; + } + return $this->searchResultFactory->create( [ 'totalCount' => $searchResult->getTotalCount(), 'productsSearchResult' => $searchResult->getProductsSearchResult(), - 'searchAggregation' => $aggregation + 'searchAggregation' => $itemsResults->getAggregations(), + 'pageSize' => $realPageSize, + 'currentPage' => $realCurrentPage, + 'totalPages' => $maxPages, ] ); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php index 849d9065d2449..e4a137413b4c5 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php @@ -53,4 +53,34 @@ public function getSearchAggregation(): ?AggregationInterface { return $this->data['searchAggregation'] ?? null; } + + /** + * Retrieve the page size for the search + * + * @return int + */ + public function getPageSize(): int + { + return $this->data['pageSize'] ?? 0; + } + + /** + * Retrieve the current page for the search + * + * @return int + */ + public function getCurrentPage(): int + { + return $this->data['currentPage'] ?? 0; + } + + /** + * Retrieve total pages for the search + * + * @return int + */ + public function getTotalPages(): int + { + return $this->data['totalPages'] ?? 0; + } } diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 1ce46cf3cbef9..ea56faf94408e 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -315,9 +315,6 @@ input ProductFilterInput @doc(description: "ProductFilterInput defines the filte country_of_manufacture: FilterTypeInput @doc(description: "The product's country of origin.") custom_layout: FilterTypeInput @doc(description: "The name of a custom layout.") gift_message_available: FilterTypeInput @doc(description: "Indicates whether a gift message is available.") - visibility: FilterTypeInput @doc(description: "CATEGORY attribute name") - price_dynamic_algorithm: FilterTypeInput @doc(description: "CATEGORY attribute name") - category_ids: FilterTypeInput @doc(description: "CATEGORY attribute name") or: ProductFilterInput @doc(description: "The keyword required to perform a logical OR comparison.") } diff --git a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/AstConverter.php b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/AstConverter.php index 802d46eafcb97..baf165b0298c3 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/AstConverter.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/AstConverter.php @@ -59,17 +59,10 @@ public function __construct( public function getClausesFromAst(string $fieldName, array $arguments) : array { $attributes = $this->fieldEntityAttributesPool->getEntityAttributesForEntityFromField($fieldName); - $attributes[] = 'cat'; - $attributes[] = 'mysize'; - $attributes[] = 'mycolor'; - $attributes[] = 'ca_1_631447041'; - $attributes[] = 'attributeset2attribute1'; - $attributes[] = 'price_dynamic_algorithm'; - $attributes[] = 'visibility'; - $attributes[] = 'category_ids'; $conditions = []; foreach ($arguments as $argumentName => $argument) { - if (in_array($argumentName, $attributes)) { + if (key_exists($argumentName, $attributes)) { + $argumentName = $attributes[$argumentName]['fieldName'] ?? $argumentName; foreach ($argument as $clauseType => $clause) { if (is_array($clause)) { $value = []; From 2df1ba3712b841c334374b123d311e2841111662 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Tue, 13 Aug 2019 09:08:22 -0500 Subject: [PATCH 008/147] MC-19102: Adding new search request and use search retrieve results - Add new search_request types for graphql product search --- .../Product/SearchCriteriaBuilder.php | 4 +- .../Model/Resolver/Products.php | 3 +- .../Plugin/Search/Request/ConfigReader.php | 221 ++++++++++++++++++ app/code/Magento/CatalogGraphQl/etc/di.xml | 4 + .../CatalogGraphQl/etc/search_request.xml | 90 +++++++ 5 files changed, 319 insertions(+), 3 deletions(-) create mode 100644 app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php create mode 100644 app/code/Magento/CatalogGraphQl/etc/search_request.xml diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php index f49db706e3aaf..6970c13aecbc6 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -74,7 +74,9 @@ public function __construct( public function build(array $args, bool $includeAggregation): SearchCriteriaInterface { $searchCriteria = $this->builder->build('products', $args); - $searchCriteria->setRequestName('catalog_view_container'); + $searchCriteria->setRequestName( + $includeAggregation ? 'graphql_product_search_with_aggregation' : 'graphql_product_search' + ); if ($includeAggregation) { $this->preparePriceAggregation($searchCriteria); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php index 10d0d10747eaf..9c8d3266c4d2d 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php @@ -56,7 +56,7 @@ class Products implements ResolverInterface * @param Search $searchQuery * @param Filter $filterQuery * @param SearchFilter $searchFilter - * @param SearchCriteriaBuilder|null $searchCriteriaBuilder + * @param SearchCriteriaBuilder|null $searchApiCriteriaBuilder */ public function __construct( Builder $searchCriteriaBuilder, @@ -106,7 +106,6 @@ public function resolve( $searchCriteria->setCurrentPage($args['currentPage']); $searchCriteria->setPageSize($args['pageSize']); - $searchResult = $this->searchQuery->getResult($searchCriteria, $info); if ($searchResult->getCurrentPage() > $searchResult->getTotalPages() && $searchResult->getTotalCount() > 0) { diff --git a/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php new file mode 100644 index 0000000000000..70e312ff4e2ee --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php @@ -0,0 +1,221 @@ +dataProvider = $dataProvider; + $this->generatorResolver = $generatorResolver; + } + + /** + * Merge reader's value with generated + * + * @param \Magento\Framework\Config\ReaderInterface $subject + * @param array $result + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterRead( + \Magento\Framework\Config\ReaderInterface $subject, + array $result + ) { + $searchRequestNameWithAggregation = $this->generateRequest(); + $searchRequest = $searchRequestNameWithAggregation; + $searchRequest['queries'][$this->requestName] = $searchRequest['queries'][$this->requestNameWithAggregation]; + unset($searchRequest['queries'][$this->requestNameWithAggregation], $searchRequest['aggregations']); + + return array_merge_recursive( + $result, + [ + $this->requestNameWithAggregation => $searchRequestNameWithAggregation, + $this->requestName => $searchRequest, + ] + ); + } + + /** + * Retrieve searchable attributes + * + * @return \Magento\Eav\Model\Entity\Attribute[] + */ + private function getSearchableAttributes(): array + { + $attributes = []; + foreach ($this->dataProvider->getSearchableAttributes() as $attribute) { + $attributes[$attribute->getAttributeCode()] = $attribute; + } + return $attributes; + } + + /** + * Generate search request for search products via GraphQL + * + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function generateRequest() + { + $request = []; + foreach ($this->getSearchableAttributes() as $attribute) { + if (\in_array($attribute->getAttributeCode(), ['price', 'visibility', 'category_ids'])) { + //same fields have special semantics + continue; + } + $queryName = $attribute->getAttributeCode() . '_query'; + $request['queries'][$this->requestNameWithAggregation]['queryReference'][] = [ + 'clause' => 'must', + 'ref' => $queryName, + ]; + switch ($attribute->getBackendType()) { + case 'static': + case 'text': + case 'varchar': + if ($attribute->getFrontendInput() === 'multiselect') { + $filterName = $attribute->getAttributeCode() . RequestGenerator::FILTER_SUFFIX; + $request['queries'][$queryName] = [ + 'name' => $queryName, + 'type' => QueryInterface::TYPE_FILTER, + 'filterReference' => [ + [ + 'ref' => $filterName, + ], + ], + ]; + $request['filters'][$filterName] = [ + 'type' => FilterInterface::TYPE_TERM, + 'name' => $filterName, + 'field' => $attribute->getAttributeCode(), + 'value' => '$' . $attribute->getAttributeCode() . '$', + ]; + } else { + $request['queries'][$queryName] = [ + 'name' => $queryName, + 'type' => 'matchQuery', + 'value' => '$' . $attribute->getAttributeCode() . '$', + 'match' => [ + [ + 'field' => $attribute->getAttributeCode(), + 'boost' => $attribute->getSearchWeight() ?: 1, + ], + ], + ]; + } + break; + case 'decimal': + case 'datetime': + case 'date': + $filterName = $attribute->getAttributeCode() . RequestGenerator::FILTER_SUFFIX; + $request['queries'][$queryName] = [ + 'name' => $queryName, + 'type' => QueryInterface::TYPE_FILTER, + 'filterReference' => [ + [ + 'ref' => $filterName, + ], + ], + ]; + $request['filters'][$filterName] = [ + 'field' => $attribute->getAttributeCode(), + 'name' => $filterName, + 'type' => FilterInterface::TYPE_RANGE, + 'from' => '$' . $attribute->getAttributeCode() . '.from$', + 'to' => '$' . $attribute->getAttributeCode() . '.to$', + ]; + break; + default: + $filterName = $attribute->getAttributeCode() . RequestGenerator::FILTER_SUFFIX; + $request['queries'][$queryName] = [ + 'name' => $queryName, + 'type' => QueryInterface::TYPE_FILTER, + 'filterReference' => [ + [ + 'ref' => $filterName, + ], + ], + ]; + $request['filters'][$filterName] = [ + 'type' => FilterInterface::TYPE_TERM, + 'name' => $filterName, + 'field' => $attribute->getAttributeCode(), + 'value' => '$' . $attribute->getAttributeCode() . '$', + ]; + } + $generator = $this->generatorResolver->getGeneratorForType($attribute->getBackendType()); + + if ($attribute->getData(EavAttributeInterface::IS_FILTERABLE)) { + $bucketName = $attribute->getAttributeCode() . self::BUCKET_SUFFIX; + $request['aggregations'][$bucketName] = $generator->getAggregationData($attribute, $bucketName); + } + + $this->addSearchAttributeToFullTextSearch($attribute, $request); + } + + return $request; + } + + /** + * Add attribute with specified boost to "search" query used in full text search + * + * @param \Magento\Eav\Model\Entity\Attribute $attribute + * @param array $request + * @return void + */ + private function addSearchAttributeToFullTextSearch(\Magento\Eav\Model\Entity\Attribute $attribute, &$request): void + { + // Match search by custom price attribute isn't supported + if ($attribute->getFrontendInput() !== 'price') { + $request['queries']['search']['match'][] = [ + 'field' => $attribute->getAttributeCode(), + 'boost' => $attribute->getSearchWeight() ?: 1, + ]; + } + } +} diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml index cea1c7ce327e2..0fe30eb0503ea 100644 --- a/app/code/Magento/CatalogGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/di.xml @@ -57,4 +57,8 @@ Magento\Catalog\Model\Api\SearchCriteria\ProductCollectionProcessor + + + + diff --git a/app/code/Magento/CatalogGraphQl/etc/search_request.xml b/app/code/Magento/CatalogGraphQl/etc/search_request.xml new file mode 100644 index 0000000000000..ab1eea9eb6fda --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/etc/search_request.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 10000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 10000 + + From 8d9424cbd35f134a15f4ab323a4e913d5fc15482 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Tue, 13 Aug 2019 12:56:54 -0500 Subject: [PATCH 009/147] MC-18512: Dynamically inject all searchable custom attributes for product filtering --- .../CatalogGraphQl/Model/Config/FilterAttributeReader.php | 4 ++++ .../CatalogGraphQl/Model/Config/SortAttributeReader.php | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php index b84c18ea5ac0b..1dd218329112c 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php +++ b/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php @@ -67,6 +67,10 @@ public function read($scope = null) : array $config = []; foreach ($this->getAttributeCollection() as $attribute) { + if (!$attribute->getIsUserDefined()) { + //do not override fields defined in schema.graphqls + continue; + } $attributeCode = $attribute->getAttributeCode(); foreach ($typeNames as $typeName) { diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php index 215b28be0579c..079084e95b9de 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php +++ b/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php @@ -61,6 +61,10 @@ public function read($scope = null) : array $attributes = $this->attributesCollection->addSearchableAttributeFilter()->addFilter('used_for_sort_by', 1); /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ foreach ($attributes as $attribute) { + if (!$attribute->getIsUserDefined()) { + //do not override fields defined in schema.graphqls + continue; + } $attributeCode = $attribute->getAttributeCode(); $attributeLabel = $attribute->getDefaultFrontendLabel(); foreach ($map as $type) { From f326e0789e937f13cbb45fed0a5597ca6548a984 Mon Sep 17 00:00:00 2001 From: Deepty Thampy Date: Tue, 13 Aug 2019 13:39:25 -0500 Subject: [PATCH 010/147] MC-18514: API functional test to cover filterable custom attributes in layered navigation - added data fixture --- .../attribute_set_based_on_default_set.php | 26 ++++ ...th_layered_navigation_custom_attribute.php | 144 ++++++++++++++++++ ...d_navigation_custom_attribute_rollback.php | 46 ++++++ 3 files changed, 216 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_set.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_set.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_set.php new file mode 100644 index 0000000000000..929b88367dd78 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_set.php @@ -0,0 +1,26 @@ +create(\Magento\Eav\Model\Entity\Attribute\Set::class); + +$entityType = $objectManager->create(\Magento\Eav\Model\Entity\Type::class)->loadByCode('catalog_product'); +$defaultSetId = $objectManager->create(\Magento\Catalog\Model\Product::class)->getDefaultAttributeSetid(); + +$data = [ + 'attribute_set_name' => 'second_attribute_set', + 'entity_type_id' => $entityType->getId(), + 'sort_order' => 200, +]; + +$attributeSet->setData($data); +$attributeSet->validate(); +$attributeSet->save(); +$attributeSet->initFromSkeleton($defaultSetId); +$attributeSet->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php new file mode 100644 index 0000000000000..00678ae904def --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php @@ -0,0 +1,144 @@ +get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); + +$eavConfig->clear(); + +$attribute1 = $eavConfig->getAttribute('catalog_product', ' second_test_configurable'); +$eavConfig->clear(); + +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); + +if (!$attribute->getId()) { + + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $attribute->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default' => ['option_0'] + ] + ); + + $attributeRepository->save($attribute); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); + CacheCleaner::cleanAll(); +} +// create a second attribute +if (!$attribute1->getId()) { + + /** @var $attribute1 \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute1 = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $attribute1->setData( + [ + 'attribute_code' => 'second_test_configurable', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Second Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 3'], 'option_1' => ['Option 4']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default' => ['option_0'] + ] + ); + + $attributeRepository->save($attribute1); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', $attributeSet->getId(), $attributeSet->getDefaultGroupId(), $attribute1->getId()); + CacheCleaner::cleanAll(); +} + +$eavConfig->clear(); + +/** @var \Magento\Framework\ObjectManagerInterface $objectManager */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var $productRepository \Magento\Catalog\Api\ProductRepositoryInterface */ +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productsWithNewAttributeSet = ['simple', 'simple-4']; + +foreach ($productsWithNewAttributeSet as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + $product->setAttributeSetId($attributeSet->getId()); + $productRepository->save($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + + } +} +/** @var \Magento\Indexer\Model\Indexer\Collection $indexerCollection */ +$indexerCollection = Bootstrap::getObjectManager()->get(\Magento\Indexer\Model\Indexer\Collection::class); +$indexerCollection->load(); +/** @var \Magento\Indexer\Model\Indexer $indexer */ +foreach ($indexerCollection->getItems() as $indexer) { + $indexer->reindexAll(); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute_rollback.php new file mode 100644 index 0000000000000..4212075c312c9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute_rollback.php @@ -0,0 +1,46 @@ +get(\Magento\Eav\Model\Config::class); +$attributesToDelete = ['test_configurable', 'second_test_configurable']; +/** @var AttributeRepositoryInterface $attributeRepository */ +$attributeRepository = Bootstrap::getObjectManager()->get(AttributeRepositoryInterface::class); + +foreach ($attributesToDelete as $attributeCode) { + /** @var \Magento\Eav\Api\Data\AttributeInterface $attribute */ + $attribute = $attributeRepository->get('catalog_product', $attributeCode); + $attributeRepository->delete($attribute); +} +/** @var $product \Magento\Catalog\Model\Product */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$entityType = $objectManager->create(\Magento\Eav\Model\Entity\Type::class)->loadByCode('catalog_product'); + +// remove attribute set + +/** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection $attributeSetCollection */ +$attributeSetCollection = $objectManager->create( + \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection::class +); +$attributeSetCollection->addFilter('attribute_set_name', 'second_attribute_set'); +$attributeSetCollection->addFilter('entity_type_id', $entityType->getId()); +$attributeSetCollection->setOrder('attribute_set_id'); // descending is default value +$attributeSetCollection->setPageSize(1); +$attributeSetCollection->load(); + +/** @var \Magento\Eav\Model\Entity\Attribute\Set $attributeSet */ +$attributeSet = $attributeSetCollection->fetchItem(); +$attributeSet->delete(); From 0af4c6c425cf2e1b7dbebbc1ad2aee753a3ead29 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Tue, 13 Aug 2019 16:08:26 -0500 Subject: [PATCH 011/147] MC-19102: Adding new search request and use search retrieve results --- .../Product/SearchCriteriaBuilder.php | 28 +++++++++++++++++-- .../CatalogGraphQl/etc/search_request.xml | 4 +-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php index 6970c13aecbc6..9e381307d8139 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -10,9 +10,11 @@ use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\Search\FilterGroupBuilder; use Magento\Framework\Api\Search\SearchCriteriaInterface; +use Magento\Framework\Api\SortOrder; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\Builder; use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\Api\SortOrderBuilder; /** * Build search criteria @@ -43,25 +45,33 @@ class SearchCriteriaBuilder */ private $visibility; + /** + * @var SortOrderBuilder + */ + private $sortOrderBuilder; + /** * @param Builder $builder * @param ScopeConfigInterface $scopeConfig * @param FilterBuilder $filterBuilder * @param FilterGroupBuilder $filterGroupBuilder * @param Visibility $visibility + * @param SortOrderBuilder $sortOrderBuilder */ public function __construct( Builder $builder, ScopeConfigInterface $scopeConfig, FilterBuilder $filterBuilder, FilterGroupBuilder $filterGroupBuilder, - Visibility $visibility + Visibility $visibility, + SortOrderBuilder $sortOrderBuilder ) { $this->scopeConfig = $scopeConfig; $this->filterBuilder = $filterBuilder; $this->filterGroupBuilder = $filterGroupBuilder; $this->builder = $builder; $this->visibility = $visibility; + $this->sortOrderBuilder = $sortOrderBuilder; } /** @@ -84,7 +94,7 @@ public function build(array $args, bool $includeAggregation): SearchCriteriaInte if (!empty($args['search'])) { $this->addFilter($searchCriteria, 'search_term', $args['search']); if (!$searchCriteria->getSortOrders()) { - $searchCriteria->setSortOrders(['_score' => \Magento\Framework\Api\SortOrder::SORT_DESC]); + $this->addDefaultSortOrder($searchCriteria); } } @@ -151,4 +161,18 @@ private function addFilter(SearchCriteriaInterface $searchCriteria, string $fiel $filterGroups[] = $this->filterGroupBuilder->create(); $searchCriteria->setFilterGroups($filterGroups); } + + /** + * Sort by _score DESC if no sort order is set + * + * @param SearchCriteriaInterface $searchCriteria + */ + private function addDefaultSortOrder(SearchCriteriaInterface $searchCriteria): void + { + $sortOrder = $this->sortOrderBuilder + ->setField('_score') + ->setDirection(SortOrder::SORT_DESC) + ->create(); + $searchCriteria->setSortOrders([$sortOrder]); + } } diff --git a/app/code/Magento/CatalogGraphQl/etc/search_request.xml b/app/code/Magento/CatalogGraphQl/etc/search_request.xml index ab1eea9eb6fda..5e962d8467a4f 100644 --- a/app/code/Magento/CatalogGraphQl/etc/search_request.xml +++ b/app/code/Magento/CatalogGraphQl/etc/search_request.xml @@ -34,7 +34,7 @@ - + @@ -80,7 +80,7 @@ - + From f07847f86b7d335aed937b4160d6156dcb2950b3 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Wed, 14 Aug 2019 11:56:18 -0500 Subject: [PATCH 012/147] MC-19102: Adding new search request and use search retrieve results --- .../Model/Resolver/Category/Products.php | 41 ++++++++++++++----- .../Model/Resolver/Products.php | 3 -- .../Model/Resolver/Products/Query/Search.php | 2 + 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php index e0580213ddea7..abc5ae7e1da7f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php @@ -8,6 +8,9 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Category; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\CatalogGraphQl\DataProvider\Product\SearchCriteriaBuilder; +use Magento\CatalogGraphQl\Model\Resolver\Products\Query\Search; +use Magento\Framework\App\ObjectManager; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; @@ -27,27 +30,46 @@ class Products implements ResolverInterface /** * @var Builder + * @deprecated */ private $searchCriteriaBuilder; /** * @var Filter + * @deprecated */ private $filterQuery; + /** + * @var Search + */ + private $searchQuery; + + /** + * @var SearchCriteriaBuilder + */ + private $searchApiCriteriaBuilder; + /** * @param ProductRepositoryInterface $productRepository * @param Builder $searchCriteriaBuilder * @param Filter $filterQuery + * @param Search $searchQuery + * @param SearchCriteriaBuilder $searchApiCriteriaBuilder */ public function __construct( ProductRepositoryInterface $productRepository, Builder $searchCriteriaBuilder, - Filter $filterQuery + Filter $filterQuery, + Search $searchQuery = null, + SearchCriteriaBuilder $searchApiCriteriaBuilder = null ) { $this->productRepository = $productRepository; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->filterQuery = $filterQuery; + $this->searchQuery = $searchQuery ?? ObjectManager::getInstance()->get(Search::class); + $this->searchApiCriteriaBuilder = $searchApiCriteriaBuilder ?? + ObjectManager::getInstance()->get(SearchCriteriaBuilder::class); } /** @@ -60,21 +82,20 @@ public function resolve( array $value = null, array $args = null ) { - $args['filter'] = [ - 'category_id' => [ - 'eq' => $value['id'] - ] - ]; - $searchCriteria = $this->searchCriteriaBuilder->build($field->getName(), $args); if ($args['currentPage'] < 1) { throw new GraphQlInputException(__('currentPage value must be greater than 0.')); } if ($args['pageSize'] < 1) { throw new GraphQlInputException(__('pageSize value must be greater than 0.')); } - $searchCriteria->setCurrentPage($args['currentPage']); - $searchCriteria->setPageSize($args['pageSize']); - $searchResult = $this->filterQuery->getResult($searchCriteria, $info); + + $args['filter'] = [ + 'category_id' => [ + 'eq' => $value['id'] + ] + ]; + $searchCriteria = $this->searchApiCriteriaBuilder->build($args, false); + $searchResult = $this->searchQuery->getResult($searchCriteria, $info); //possible division by 0 if ($searchCriteria->getPageSize()) { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php index 9c8d3266c4d2d..bb94502c41ef1 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php @@ -103,9 +103,6 @@ public function resolve( isset($productFields['filters']) ); - $searchCriteria->setCurrentPage($args['currentPage']); - $searchCriteria->setPageSize($args['pageSize']); - $searchResult = $this->searchQuery->getResult($searchCriteria, $info); if ($searchResult->getCurrentPage() > $searchResult->getTotalPages() && $searchResult->getTotalCount() > 0) { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index daa3619d17b83..7d55b56f854a2 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -120,6 +120,8 @@ public function getResult(SearchCriteriaInterface $searchCriteria, ResolveInfo $ $searchResult = $this->filterQuery->getResult($searchCriteriaIds, $info, true); + $searchCriteria->setPageSize($realPageSize); + $searchCriteria->setCurrentPage($realCurrentPage); //possible division by 0 if ($realPageSize) { $maxPages = (int)ceil($searchResult->getTotalCount() / $realPageSize); From 03a990747b74019321ab6077e4691635358a9a24 Mon Sep 17 00:00:00 2001 From: Deepty Thampy Date: Wed, 14 Aug 2019 13:11:47 -0500 Subject: [PATCH 013/147] MC-18514: API functional test to cover filterable custom attributes in layered navigation - updated data fixture --- .../products_with_layered_navigation_custom_attribute.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php index 00678ae904def..d913e3075622e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php @@ -124,12 +124,13 @@ /** @var $productRepository \Magento\Catalog\Api\ProductRepositoryInterface */ $productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); -$productsWithNewAttributeSet = ['simple', 'simple-4']; +$productsWithNewAttributeSet = ['simple', '12345', 'simple-4']; foreach ($productsWithNewAttributeSet as $sku) { try { $product = $productRepository->get($sku, false, null, true); $product->setAttributeSetId($attributeSet->getId()); + $product->setStockData(['use_config_manage_stock' => 1, 'qty' => 50, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); $productRepository->save($product); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { From ae7016a8ef7221bc8512791b1e678f442ccf82ab Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Thu, 15 Aug 2019 11:38:46 -0500 Subject: [PATCH 014/147] MC-19102: Adding new search request and use search retrieve results --- .../Model/Resolver/Products.php | 2 +- .../Model/Resolver/Products/Query/Search.php | 11 ++++-- .../Plugin/Search/Request/ConfigReader.php | 35 ++++++++++++------- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php index bb94502c41ef1..6a17f730c0f1a 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php @@ -103,7 +103,7 @@ public function resolve( isset($productFields['filters']) ); - $searchResult = $this->searchQuery->getResult($searchCriteria, $info); + $searchResult = $this->searchQuery->getResult($searchCriteria, $info, $args); if ($searchResult->getCurrentPage() > $searchResult->getTotalPages() && $searchResult->getTotalCount() > 0) { throw new GraphQlInputException( diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index 7d55b56f854a2..c7dd2a8b05b79 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -87,15 +87,20 @@ public function __construct( * * @param SearchCriteriaInterface $searchCriteria * @param ResolveInfo $info + * @param array $args * @return SearchResult * @throws \Exception */ - public function getResult(SearchCriteriaInterface $searchCriteria, ResolveInfo $info) : SearchResult - { + public function getResult( + SearchCriteriaInterface $searchCriteria, + ResolveInfo $info, + array $args = [] + ): SearchResult { $idField = $this->metadataPool->getMetadata( \Magento\Catalog\Api\Data\ProductInterface::class )->getIdentifierField(); + $isSearch = isset($args['search']); $realPageSize = $searchCriteria->getPageSize(); $realCurrentPage = $searchCriteria->getCurrentPage(); // Current page must be set to 0 and page size to max for search to grab all ID's as temporary workaround @@ -118,7 +123,7 @@ public function getResult(SearchCriteriaInterface $searchCriteria, ResolveInfo $ $searchCriteriaIds->setPageSize($realPageSize); $searchCriteriaIds->setCurrentPage($realCurrentPage); - $searchResult = $this->filterQuery->getResult($searchCriteriaIds, $info, true); + $searchResult = $this->filterQuery->getResult($searchCriteriaIds, $info, $isSearch); $searchCriteria->setPageSize($realPageSize); $searchCriteria->setCurrentPage($realCurrentPage); diff --git a/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php index 70e312ff4e2ee..151be89177744 100644 --- a/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php +++ b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php @@ -6,11 +6,11 @@ namespace Magento\CatalogGraphQl\Plugin\Search\Request; use Magento\Catalog\Api\Data\EavAttributeInterface; -use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider; use Magento\CatalogSearch\Model\Search\RequestGenerator; use Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorResolver; use Magento\Framework\Search\Request\FilterInterface; use Magento\Framework\Search\Request\QueryInterface; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; /** * Add search request configuration to config for give ability filter and search products during GraphQL request @@ -33,26 +33,28 @@ class ConfigReader private $requestName = 'graphql_product_search'; /** - * @var DataProvider + * @var GeneratorResolver */ - private $dataProvider; + private $generatorResolver; /** - * @var GeneratorResolver + * @var CollectionFactory */ - private $generatorResolver; + private $productAttributeCollectionFactory; /** Bucket name suffix */ private const BUCKET_SUFFIX = '_bucket'; /** - * @param DataProvider $dataProvider * @param GeneratorResolver $generatorResolver + * @param CollectionFactory $productAttributeCollectionFactory */ - public function __construct(DataProvider $dataProvider, GeneratorResolver $generatorResolver) - { - $this->dataProvider = $dataProvider; + public function __construct( + GeneratorResolver $generatorResolver, + CollectionFactory $productAttributeCollectionFactory + ) { $this->generatorResolver = $generatorResolver; + $this->productAttributeCollectionFactory = $productAttributeCollectionFactory; } /** @@ -89,9 +91,18 @@ public function afterRead( private function getSearchableAttributes(): array { $attributes = []; - foreach ($this->dataProvider->getSearchableAttributes() as $attribute) { - $attributes[$attribute->getAttributeCode()] = $attribute; + /** @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection $productAttributes */ + $productAttributes = $this->productAttributeCollectionFactory->create(); + $productAttributes->addFieldToFilter( + ['is_searchable', 'is_visible_in_advanced_search', 'is_filterable', 'is_filterable_in_search'], + [1, 1, [1, 2], 1] + ); + + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ + foreach ($productAttributes->getItems() as $attribute) { + $attributes[$attribute->getAttributeCode()] = $attribute; } + return $attributes; } @@ -106,7 +117,7 @@ private function generateRequest() $request = []; foreach ($this->getSearchableAttributes() as $attribute) { if (\in_array($attribute->getAttributeCode(), ['price', 'visibility', 'category_ids'])) { - //same fields have special semantics + //some fields have special semantics continue; } $queryName = $attribute->getAttributeCode() . '_query'; From 3c6eec41102cdccb609d0345b01563160e9ad931 Mon Sep 17 00:00:00 2001 From: Deepty Thampy Date: Thu, 15 Aug 2019 12:17:28 -0500 Subject: [PATCH 015/147] MC-18514: API functional test to cover filterable custom attributes in layered navigation - added use cases --- .../GraphQl/Catalog/ProductSearchTest.php | 267 ++++++++++++++++++ ...th_layered_navigation_custom_attribute.php | 8 +- 2 files changed, 272 insertions(+), 3 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index 4ce8ad8dab393..e80387394ffaf 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -18,6 +18,7 @@ use Magento\TestFramework\TestCase\GraphQlAbstract; use Magento\Catalog\Model\Product; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\Framework\DataObject; /** @@ -89,6 +90,272 @@ public function testFilterLn() ); } + /** + * Advanced Search which uses product attribute to filter out the results + * + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testAdvancedSearchByOneCustomAttribute() + { + $optionValue = $this->getDefaultAttributeOptionValue('second_test_configurable'); + + $query = <<get(ProductRepositoryInterface::class); + $product1 = $productRepository->get('simple'); + $product2 = $productRepository->get('12345'); + $product3 = $productRepository->get('simple-4'); + $filteredProducts = [$product1, $product2, $product3 ]; + $response = $this->graphQlQuery($query); + $this->assertEquals(3, $response['products']['total_count']); + $this->assertTrue(count($response['products']['filters']) > 0, 'Product filters is not empty'); + $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); + // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall + for ($itemIndex = 0; $itemIndex < count($filteredProducts); $itemIndex++) { + $this->assertNotEmpty($productItemsInResponse[$itemIndex]); + //validate that correct products are returned + $this->assertResponseFields( + $productItemsInResponse[$itemIndex][0], + [ 'name' => $filteredProducts[$itemIndex]->getName(), + 'sku' => $filteredProducts[$itemIndex]->getSku() + ] + ); + } + + /** @var \Magento\Eav\Model\Config $eavConfig */ + $eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); + $attribute = $eavConfig->getAttribute('catalog_product', 'second_test_configurable'); + + // Validate custom attribute filter layer data + $this->assertResponseFields( + $response['products']['filters'][2], + [ + 'name' => $attribute->getDefaultFrontendLabel(), + 'request_var'=> $attribute->getAttributeCode(), + 'filter_items_count'=> 1, + 'filter_items' => [ + [ + 'label' => 'Option 3', + 'items_count' => 3, + 'value_string' => $optionValue, + '__typename' =>'LayerFilterItem' + ], + ], + ] + ); + } + + /** + * Get the option value for the custom attribute to be used in the graphql query + * + * @param string $attributeCode + * @return string + */ + private function getDefaultAttributeOptionValue(string $attributeCode) : string + { + /** @var \Magento\Eav\Model\Config $eavConfig */ + $eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); + $attribute = $eavConfig->getAttribute('catalog_product', $attributeCode); + /** @var AttributeOptionInterface[] $options */ + $options = $attribute->getOptions(); + $defaultOptionValue = $options[1]->getValue(); + return $defaultOptionValue; + } + + /** + * Full text search for Product and then filter the results by custom attribute + * + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testFullTextSearchForProductAndFilterByCustomAttribute() + { + $optionValue = $this->getDefaultAttributeOptionValue('second_test_configurable'); + + $query = <<graphQlQuery($query); + //Verify total count of the products returned + $this->assertEquals(3, $response['products']['total_count']); + $expectedFilterLayers = + [ + ['name' => 'Price', + 'request_var'=> 'price' + ], + ['name' => 'Category', + 'request_var'=> 'category_id' + ], + ['name' => 'Second Test Configurable', + 'request_var'=> 'second_test_configurable' + ], + ]; + $layers = array_map(null, $expectedFilterLayers, $response['products']['filters']); + + //Verify all the three layers : Price, Category and Custom attribute layers are created + foreach ($layers as $layerIndex => $layerFilterData) { + $this->assertNotEmpty($layerFilterData); + $this->assertEquals( + $layers[$layerIndex][0]['name'], + $response['products']['filters'][$layerIndex]['name'], + 'Layer name does not match' + ); + $this->assertEquals( + $layers[$layerIndex][0]['request_var'], + $response['products']['filters'][$layerIndex]['request_var'], + 'request_var does not match' + ) ; + } + + // Validate the price filter layer data from the response + $this->assertResponseFields( + $response['products']['filters'][0], + [ + 'name' => 'Price', + 'request_var'=> 'price', + 'filter_items_count'=> 2, + 'filter_items' => [ + [ + 'label' => '10-20', + 'items_count' => 2, + 'value_string' => '10_20', + '__typename' =>'LayerFilterItem' + ], + [ + 'label' => '40-*', + 'items_count' => 1, + 'value_string' => '40_*', + '__typename' =>'LayerFilterItem' + ], + ], + ] + ); + } + + /** + * Filter by mu;ltiple attributes like category_id and custom attribute + * + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testFilterByCategoryIdAndCustomAttribute() + { + $categoryId = 13; + $optionValue = $this->getDefaultAttributeOptionValue('second_test_configurable'); + $query = <<graphQlQuery($query); + $this->assertEquals(2, $response['products']['total_count']); + } + + + /** * Get array with expected data for layered navigation filters * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php index d913e3075622e..9167bbc1db092 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php @@ -68,7 +68,6 @@ /* Assign attribute to attribute set */ $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); - CacheCleaner::cleanAll(); } // create a second attribute if (!$attribute1->getId()) { @@ -113,8 +112,11 @@ $attributeRepository->save($attribute1); /* Assign attribute to attribute set */ - $installer->addAttributeToGroup('catalog_product', $attributeSet->getId(), $attributeSet->getDefaultGroupId(), $attribute1->getId()); - CacheCleaner::cleanAll(); + $installer->addAttributeToGroup('catalog_product', + $attributeSet->getId(), + $attributeSet->getDefaultGroupId(), + $attribute1->getId() + ); } $eavConfig->clear(); From 35a868adf054a39160b53fe3abb663b6b0a452bb Mon Sep 17 00:00:00 2001 From: Deepty Thampy Date: Thu, 15 Aug 2019 16:44:12 -0500 Subject: [PATCH 016/147] MC-18514: API functional test to cover filterable custom attributes in layered navigation - added use cases --- .../GraphQl/Catalog/ProductSearchTest.php | 45 ++++++++++++++++++- ...th_layered_navigation_custom_attribute.php | 14 +++--- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index e80387394ffaf..e607e2d608786 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -258,7 +258,7 @@ public function testFullTextSearchForProductAndFilterByCustomAttribute() 'request_var'=> 'category_id' ], ['name' => 'Second Test Configurable', - 'request_var'=> 'second_test_configurable' + 'request_var'=> 'second_test_configurable' ], ]; $layers = array_map(null, $expectedFilterLayers, $response['products']['filters']); @@ -352,6 +352,49 @@ public function testFilterByCategoryIdAndCustomAttribute() QUERY; $response = $this->graphQlQuery($query); $this->assertEquals(2, $response['products']['total_count']); + $actualCategoryFilterItems = $response['products']['filters'][1]['filter_items']; + //Validate the number of categories/sub-categories that contain the products with the custom attribute + $this->assertCount(6,$actualCategoryFilterItems); + + $expectedCategoryFilterItems = + [ + [ 'label' => 'Category 1', + 'items_count'=> 2 + ], + [ 'label' => 'Category 1.1', + 'items_count'=> 1 + ], + [ 'label' => 'Movable Position 2', + 'items_count'=> 1 + ], + [ 'label' => 'Movable Position 3', + 'items_count'=> 1 + ], + [ 'label' => 'Category 12', + 'items_count'=> 1 + ], + [ 'label' => 'Category 1.2', + 'items_count'=> 2 + ], + ]; + $categoryFilterItems = array_map(null, $expectedCategoryFilterItems,$actualCategoryFilterItems); + +//Validate the categories and sub-categories data in the filter layer + foreach ($categoryFilterItems as $index => $categoryFilterData) { + $this->assertNotEmpty($categoryFilterData); + $this->assertEquals( + $categoryFilterItems[$index][0]['label'], + $actualCategoryFilterItems[$index]['label'], + 'Category is incorrect' + ); + $this->assertEquals( + $categoryFilterItems[$index][0]['items_count'], + $actualCategoryFilterItems[$index]['items_count'], + 'Products count in the category is incorrect' + ) ; + } + + } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php index 9167bbc1db092..6cd2819c2d296 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php @@ -12,7 +12,6 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\Eav\Api\AttributeRepositoryInterface; -use Magento\TestFramework\Helper\CacheCleaner; $eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); $attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); @@ -112,11 +111,11 @@ $attributeRepository->save($attribute1); /* Assign attribute to attribute set */ - $installer->addAttributeToGroup('catalog_product', + $installer->addAttributeToGroup( + 'catalog_product', $attributeSet->getId(), $attributeSet->getDefaultGroupId(), - $attribute1->getId() - ); + $attribute1->getId()); } $eavConfig->clear(); @@ -132,7 +131,12 @@ try { $product = $productRepository->get($sku, false, null, true); $product->setAttributeSetId($attributeSet->getId()); - $product->setStockData(['use_config_manage_stock' => 1, 'qty' => 50, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $product->setStockData( + ['use_config_manage_stock' => 1, + 'qty' => 50, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1] + ); $productRepository->save($product); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { From 1b626a590dca46470f73f7bd05c8ad73d2ac0fe5 Mon Sep 17 00:00:00 2001 From: Deepty Thampy Date: Fri, 16 Aug 2019 17:36:28 -0500 Subject: [PATCH 017/147] MC-18514: API functional test to cover filterable custom attributes in layered navigation - added configurable attribute use case and added fixture --- .../GraphQl/Catalog/ProductSearchTest.php | 116 +++++++++++++++++- ...th_custom_attribute_layered_navigation.php | 48 ++++++++ ..._attribute_layered_navigation_rollback.php | 9 ++ 3 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation_rollback.php diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index e607e2d608786..286843e9b53e9 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -20,6 +20,7 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\Framework\DataObject; +use Magento\TestFramework\Helper\Bootstrap; /** * @SuppressWarnings(PHPMD.TooManyPublicMethods) @@ -99,12 +100,11 @@ public function testFilterLn() public function testAdvancedSearchByOneCustomAttribute() { $optionValue = $this->getDefaultAttributeOptionValue('second_test_configurable'); - $query = <<get(ProductRepositoryInterface::class); $product1 = $productRepository->get('simple'); @@ -194,7 +196,8 @@ private function getDefaultAttributeOptionValue(string $attributeCode) : string $attribute = $eavConfig->getAttribute('catalog_product', $attributeCode); /** @var AttributeOptionInterface[] $options */ $options = $attribute->getOptions(); - $defaultOptionValue = $options[1]->getValue(); + array_shift($options); + $defaultOptionValue = $options[0]->getValue(); return $defaultOptionValue; } @@ -393,10 +396,115 @@ public function testFilterByCategoryIdAndCustomAttribute() 'Products count in the category is incorrect' ) ; } + } + private function getQueryProductsWithCustomAttribute($optionValue) + { + return <<get(\Magento\Eav\Model\Config::class); + $attribute = $eavConfig->getAttribute('catalog_product', $attributeCode); + /** @var AttributeOptionInterface[] $options */ + $options = $attribute->getOptions(); + array_shift($options); + $firstOption = $options[0]->getValue(); + $secondOption = $options[1]->getValue(); + $query = $this->getQueryProductsWithCustomAttribute($firstOption); + $response = $this->graphQlQuery($query); + + //Only 1 product will be returned since only one child product with attribute option1 from 1st Configurable product is OOS + $this->assertEquals(1, $response['products']['total_count']); + + // Custom attribute filter layer data + $this->assertResponseFields( + $response['products']['filters'][1], + [ + 'name' => $attribute->getDefaultFrontendLabel(), + 'request_var'=> $attribute->getAttributeCode(), + 'filter_items_count'=> 2, + 'filter_items' => [ + [ + 'label' => 'Option 1', + 'items_count' => 1, + 'value_string' => $firstOption, + '__typename' =>'LayerFilterItem' + ], + [ + 'label' => 'Option 2', + 'items_count' => 1, + 'value_string' => $secondOption, + '__typename' =>'LayerFilterItem' + ] + ], + ] + ); + + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $productRepository = Bootstrap::getObjectManager()->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $outOfStockChildProduct = $productRepository->get('simple_30'); + // Set another child product from 2nd Configurable product with attribute option1 to OOS + $outOfStockChildProduct->setStockData( + ['use_config_manage_stock' => 1, + 'qty' => 0, + 'is_qty_decimal' => 0, + 'is_in_stock' => 0] + ); + $productRepository->save($outOfStockChildProduct); + $query = $this->getQueryProductsWithCustomAttribute($firstOption); + $response = $this->graphQlQuery($query); + $this->assertEquals(0, $response['products']['total_count']); + $this->assertEmpty($response['products']['items']); + $this->assertEmpty($response['products']['filters']); + $i = 0; + } /** diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php new file mode 100644 index 0000000000000..03a4baabf088e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php @@ -0,0 +1,48 @@ +get(\Magento\Eav\Model\Config::class); + +/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); + +$eavConfig->clear(); + +$attribute->setIsSearchable(1) + ->setIsVisibleInAdvancedSearch(1) + ->setIsFilterable(true) + ->setIsFilterableInSearch(true) + ->setIsVisibleOnFront(1); + +/** @var AttributeRepositoryInterface $attributeRepository */ +$attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); +$attributeRepository->save($attribute); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager()->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$outOfStockChildProduct = $productRepository->get('simple_10'); +$outOfStockChildProduct->setStockData( + ['use_config_manage_stock' => 1, + 'qty' => 0, + 'is_qty_decimal' => 0, + 'is_in_stock' => 0] +); +$productRepository->save($outOfStockChildProduct); + +/** @var \Magento\Indexer\Model\Indexer\Collection $indexerCollection */ +$indexerCollection = Bootstrap::getObjectManager()->get(\Magento\Indexer\Model\Indexer\Collection::class); +$indexerCollection->load(); +/** @var \Magento\Indexer\Model\Indexer $indexer */ +foreach ($indexerCollection->getItems() as $indexer) { + $indexer->reindexAll(); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation_rollback.php new file mode 100644 index 0000000000000..49e2a8e88a1ac --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation_rollback.php @@ -0,0 +1,9 @@ + Date: Mon, 19 Aug 2019 11:51:47 -0500 Subject: [PATCH 018/147] MC-18514: API functional test to cover filterable custom attributes in layered navigation - fix existing test --- .../GraphQl/Catalog/ProductSearchTest.php | 95 ++++++------------- 1 file changed, 29 insertions(+), 66 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index 286843e9b53e9..709779e88da54 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -99,44 +99,9 @@ public function testFilterLn() */ public function testAdvancedSearchByOneCustomAttribute() { - $optionValue = $this->getDefaultAttributeOptionValue('second_test_configurable'); - $query = <<getDefaultAttributeOptionValue($attributeCode); + $query = $this->getQueryProductsWithCustomAttribute($attributeCode, $optionValue); /** @var ProductRepositoryInterface $productRepository */ $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); @@ -380,7 +345,7 @@ public function testFilterByCategoryIdAndCustomAttribute() 'items_count'=> 2 ], ]; - $categoryFilterItems = array_map(null, $expectedCategoryFilterItems,$actualCategoryFilterItems); + $categoryFilterItems = array_map(null, $expectedCategoryFilterItems, $actualCategoryFilterItems); //Validate the categories and sub-categories data in the filter layer foreach ($categoryFilterItems as $index => $categoryFilterData) { @@ -398,12 +363,12 @@ public function testFilterByCategoryIdAndCustomAttribute() } } - private function getQueryProductsWithCustomAttribute($optionValue) + private function getQueryProductsWithCustomAttribute($attributeCode, $optionValue) { return <<getValue(); $secondOption = $options[1]->getValue(); - $query = $this->getQueryProductsWithCustomAttribute($firstOption); + $query = $this->getQueryProductsWithCustomAttribute($attributeCode, $firstOption); $response = $this->graphQlQuery($query); - //Only 1 product will be returned since only one child product with attribute option1 from 1st Configurable product is OOS + //1 product will be returned since only one child product with attribute option1 from 1st Configurable product is OOS $this->assertEquals(1, $response['products']['total_count']); // Custom attribute filter layer data @@ -498,15 +463,13 @@ public function testLayeredNavigationForConfigurableProductWithOutOfStockOption( 'is_in_stock' => 0] ); $productRepository->save($outOfStockChildProduct); - $query = $this->getQueryProductsWithCustomAttribute($firstOption); + $query = $this->getQueryProductsWithCustomAttribute($attributeCode, $firstOption); $response = $this->graphQlQuery($query); $this->assertEquals(0, $response['products']['total_count']); $this->assertEmpty($response['products']['items']); $this->assertEmpty($response['products']['filters']); - $i = 0; } - /** * Get array with expected data for layered navigation filters * @@ -521,15 +484,32 @@ private function getExpectedFiltersDataSet() $options = $attribute->getOptions(); // Fetching option ID is required for continuous debug as of autoincrement IDs. return [ + [ + 'name' => 'Price', + 'filter_items_count' => 2, + 'request_var' => 'price', + 'filter_items' => [ + [ + 'label' => '*-10', + 'value_string' => '*_10', + 'items_count' => 1, + ], + [ + 'label' => '10-*', + 'value_string' => '10_*', + 'items_count' => 1, + ], + ], + ], [ 'name' => 'Category', 'filter_items_count' => 1, - 'request_var' => 'cat', + 'request_var' => 'category_id', 'filter_items' => [ [ 'label' => 'Category 1', 'value_string' => '333', - 'items_count' => 3, + 'items_count' => 2, ], ], ], @@ -544,24 +524,7 @@ private function getExpectedFiltersDataSet() 'items_count' => 1, ], ], - ], - [ - 'name' => 'Price', - 'filter_items_count' => 2, - 'request_var' => 'price', - 'filter_items' => [ - [ - 'label' => '$0.00 - $9.99', - 'value_string' => '-10', - 'items_count' => 1, - ], - [ - 'label' => '$10.00 and above', - 'value_string' => '10-', - 'items_count' => 1, - ], - ], - ], + ] ]; } From 0cd00e6e3b1f5c35a7502f45cec1bb8478b51904 Mon Sep 17 00:00:00 2001 From: Deepty Thampy Date: Mon, 19 Aug 2019 11:53:45 -0500 Subject: [PATCH 019/147] MC-18514: API functional test to cover filterable custom attributes in layered navigation - fix existing fixture --- .../_files/products_with_layered_navigation_attribute.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php index 48c47c9988d59..7bee46bc2078f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php @@ -36,7 +36,7 @@ 'is_unique' => 0, 'is_required' => 0, 'is_searchable' => 1, - 'is_visible_in_advanced_search' => 0, + 'is_visible_in_advanced_search' => 1, 'is_comparable' => 1, 'is_filterable' => 1, 'is_filterable_in_search' => 1, From 21ea200d96b86dc9261e0217915667c85f7ff6aa Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Mon, 19 Aug 2019 16:30:34 -0500 Subject: [PATCH 020/147] MC-18512: Dynamically inject all searchable custom attributes for product filtering --- .../Model/Config/FilterAttributeReader.php | 30 +++++++++++++++---- .../Model/Config/SortAttributeReader.php | 4 --- .../Magento/CatalogGraphQl/etc/graphql/di.xml | 4 +-- .../CatalogGraphQl/etc/schema.graphqls | 17 ++++++++--- app/code/Magento/GraphQl/etc/schema.graphqls | 15 ++++++++++ 5 files changed, 54 insertions(+), 16 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php index 1dd218329112c..9310dc593d40c 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php +++ b/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php @@ -30,7 +30,9 @@ class FilterAttributeReader implements ReaderInterface /** * Filter input types */ - private const FILTER_TYPE = 'FilterTypeInput'; + private const FILTER_EQUAL_TYPE = 'FilterEqualTypeInput'; + private const FILTER_RANGE_TYPE = 'FilterRangeTypeInput'; + private const FILTER_LIKE_TYPE = 'FilterLikeTypeInput'; /** * @var MapperInterface @@ -67,16 +69,12 @@ public function read($scope = null) : array $config = []; foreach ($this->getAttributeCollection() as $attribute) { - if (!$attribute->getIsUserDefined()) { - //do not override fields defined in schema.graphqls - continue; - } $attributeCode = $attribute->getAttributeCode(); foreach ($typeNames as $typeName) { $config[$typeName]['fields'][$attributeCode] = [ 'name' => $attributeCode, - 'type' => self::FILTER_TYPE, + 'type' => $this->getFilterType($attribute->getFrontendInput()), 'arguments' => [], 'required' => false, 'description' => sprintf('Attribute label: %s', $attribute->getDefaultFrontendLabel()) @@ -87,6 +85,26 @@ public function read($scope = null) : array return $config; } + /** + * Map attribute type to filter type + * + * @param string $attributeType + * @return string + */ + private function getFilterType($attributeType): string + { + $filterTypeMap = [ + 'price' => self::FILTER_RANGE_TYPE, + 'date' => self::FILTER_RANGE_TYPE, + 'select' => self::FILTER_EQUAL_TYPE, + 'boolean' => self::FILTER_EQUAL_TYPE, + 'text' => self::FILTER_LIKE_TYPE, + 'textarea' => self::FILTER_LIKE_TYPE, + ]; + + return $filterTypeMap[$attributeType] ?? self::FILTER_LIKE_TYPE; + } + /** * Create attribute collection * diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php index 079084e95b9de..215b28be0579c 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php +++ b/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php @@ -61,10 +61,6 @@ public function read($scope = null) : array $attributes = $this->attributesCollection->addSearchableAttributeFilter()->addFilter('used_for_sort_by', 1); /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ foreach ($attributes as $attribute) { - if (!$attribute->getIsUserDefined()) { - //do not override fields defined in schema.graphqls - continue; - } $attributeCode = $attribute->getAttributeCode(); $attributeLabel = $attribute->getDefaultFrontendLabel(); foreach ($map as $type) { diff --git a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml index 3ada365ec5065..ea2704f1a4ee5 100644 --- a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml @@ -49,10 +49,10 @@ CustomizableCheckboxOption - ProductSortInput + ProductAttributeSortInput - ProductFilterInput + ProductAttributeFilterInput diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index ea56faf94408e..e6daf4f4d753e 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -4,10 +4,10 @@ type Query { products ( search: String @doc(description: "Performs a full-text search using the specified key words."), - filter: ProductFilterInput @doc(description: "Identifies which product attributes to search for and return."), + filter: ProductAttributeFilterInput @doc(description: "Identifies which product attributes to search for and return."), pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), - sort: ProductSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.") + sort: ProductAttributeSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.") ): Products @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Products") @doc(description: "The products query searches for products that match the criteria specified in the search and filter attributes.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") category ( @@ -280,7 +280,11 @@ type CategoryProducts @doc(description: "The category products object returned i total_count: Int @doc(description: "The number of products returned.") } -input ProductFilterInput @doc(description: "ProductFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { +input ProductAttributeFilterInput @doc(description: "ProductAttributeFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { + category_id: FilterEqualTypeInput @doc(description: "Filter product by category id") +} + +input ProductFilterInput @deprecated(reason: "Attributes used in this input are hardcoded and some of them are not searcheable. Use @ProductAttributeFilterInput instead") @doc(description: "ProductFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { name: FilterTypeInput @doc(description: "The product name. Customers use this name to identify the product.") sku: FilterTypeInput @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") description: FilterTypeInput @doc(description: "Detailed information about the product. The value can include simple HTML tags.") @@ -333,7 +337,7 @@ type ProductMediaGalleryEntriesVideoContent @doc(description: "ProductMediaGalle video_metadata: String @doc(description: "Optional data about the video.") } -input ProductSortInput @doc(description: "ProductSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order.") { +input ProductSortInput @deprecated(reason: "Attributes used in this input are hardcoded and some of them are not searcheable. Use @ProductAttributeSortInput instead") @doc(description: "ProductSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order.") { name: SortEnum @doc(description: "The product name. Customers use this name to identify the product.") sku: SortEnum @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") description: SortEnum @doc(description: "Detailed information about the product. The value can include simple HTML tags.") @@ -367,6 +371,11 @@ input ProductSortInput @doc(description: "ProductSortInput specifies the attribu gift_message_available: SortEnum @doc(description: "Indicates whether a gift message is available.") } +input ProductAttributeSortInput @doc(description: "ProductAttributeSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order. It's possible to sort products using searchable attributes with enabled 'Use in Filter Options' option") +{ + position: SortEnum @doc(description: "The position of products") +} + type MediaGalleryEntry @doc(description: "MediaGalleryEntry defines characteristics about images and videos associated with a specific product.") { id: Int @doc(description: "The identifier assigned to the object.") media_type: String @doc(description: "image or video.") diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls index eb6a88a4d487d..997572471122f 100644 --- a/app/code/Magento/GraphQl/etc/schema.graphqls +++ b/app/code/Magento/GraphQl/etc/schema.graphqls @@ -65,6 +65,21 @@ input FilterTypeInput @doc(description: "FilterTypeInput specifies which action nin: [String] @doc(description: "Not in. The value can contain a set of comma-separated values") } +input FilterEqualTypeInput @doc(description: "Specifies which action will be performed in a query ") { + in: [String] + eq: String +} + +input FilterRangeTypeInput @doc(description: "Specifies which action will be performed in a query ") { + from: String + to: String +} + +input FilterLikeTypeInput @doc(description: "Specifies which action will be performed in a query ") { + like: String + eq: String +} + type SearchResultPageInfo @doc(description: "SearchResultPageInfo provides navigation for the query response") { page_size: Int @doc(description: "Specifies the maximum number of items to return") current_page: Int @doc(description: "Specifies which page of results to return") From 88ee6cf2824b9b12b46d82cffe6d26045f4ed74f Mon Sep 17 00:00:00 2001 From: Eden Date: Tue, 20 Aug 2019 14:53:43 +0700 Subject: [PATCH 021/147] Rss Model Refactor --- app/code/Magento/Review/Model/Rss.php | 33 ++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Review/Model/Rss.php b/app/code/Magento/Review/Model/Rss.php index df8a5dbb96841..876a3722da61e 100644 --- a/app/code/Magento/Review/Model/Rss.php +++ b/app/code/Magento/Review/Model/Rss.php @@ -3,11 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Review\Model; +use Magento\Framework\App\ObjectManager; + /** - * Class Rss - * @package Magento\Catalog\Model\Rss\Product + * Model Rss + * + * Class \Magento\Catalog\Model\Rss\Product\Rss */ class Rss extends \Magento\Framework\Model\AbstractModel { @@ -24,18 +30,39 @@ class Rss extends \Magento\Framework\Model\AbstractModel protected $eventManager; /** + * Rss constructor. + * * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param ReviewFactory $reviewFactory + * @param \Magento\Framework\Model\Context|null $context + * @param \Magento\Framework\Registry|null $registry + * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource + * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection + * @param array $data */ public function __construct( \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\Review\Model\ReviewFactory $reviewFactory + \Magento\Review\Model\ReviewFactory $reviewFactory, + \Magento\Framework\Model\Context $context = null, + \Magento\Framework\Registry $registry = null, + \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, + \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, + array $data = [] ) { $this->reviewFactory = $reviewFactory; $this->eventManager = $eventManager; + $context = $context ?? ObjectManager::getInstance()->get( + \Magento\Framework\Model\Context::class + ); + $registry = $registry ?? ObjectManager::getInstance()->get( + \Magento\Framework\Registry::class + ); + parent::__construct($context, $registry, $resource, $resourceCollection, $data); } /** + * Get Product Collection + * * @return $this|\Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection */ public function getProductCollection() From 0be11cc9db67398006ffed770eab52cea25cfb2b Mon Sep 17 00:00:00 2001 From: Eden Date: Tue, 20 Aug 2019 15:24:03 +0700 Subject: [PATCH 022/147] Fix static test rss model --- app/code/Magento/Review/Model/Rss.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/Review/Model/Rss.php b/app/code/Magento/Review/Model/Rss.php index 876a3722da61e..f5abdbb4d3c9e 100644 --- a/app/code/Magento/Review/Model/Rss.php +++ b/app/code/Magento/Review/Model/Rss.php @@ -51,12 +51,8 @@ public function __construct( ) { $this->reviewFactory = $reviewFactory; $this->eventManager = $eventManager; - $context = $context ?? ObjectManager::getInstance()->get( - \Magento\Framework\Model\Context::class - ); - $registry = $registry ?? ObjectManager::getInstance()->get( - \Magento\Framework\Registry::class - ); + $context = $context ?? ObjectManager::getInstance()->get(\Magento\Framework\Model\Context::class); + $registry = $registry ?? ObjectManager::getInstance()->get(\Magento\Framework\Registry::class); parent::__construct($context, $registry, $resource, $resourceCollection, $data); } From 3b110166c45c30265124d1ae491447337c22f5b6 Mon Sep 17 00:00:00 2001 From: Deepty Thampy Date: Tue, 20 Aug 2019 13:57:27 -0500 Subject: [PATCH 023/147] MC-18514: API functional test to cover filterable custom attributes in layered navigation - clean cache to regenerate schema --- .../GraphQl/Catalog/ProductSearchTest.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index 709779e88da54..3284a4a7172fe 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -13,6 +13,7 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\CategoryLinkManagement; +use Magento\Framework\Config\Data; use Magento\Framework\EntityManager\MetadataPool; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -21,6 +22,7 @@ use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\Framework\DataObject; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\CacheCleaner; /** * @SuppressWarnings(PHPMD.TooManyPublicMethods) @@ -37,6 +39,7 @@ class ProductSearchTest extends GraphQlAbstract */ public function testFilterLn() { + CacheCleaner::cleanAll(); $query = <<getDefaultAttributeOptionValue($attributeCode); $query = $this->getQueryProductsWithCustomAttribute($attributeCode, $optionValue); @@ -174,6 +178,7 @@ private function getDefaultAttributeOptionValue(string $attributeCode) : string */ public function testFullTextSearchForProductAndFilterByCustomAttribute() { + CacheCleaner::cleanAll(); $optionValue = $this->getDefaultAttributeOptionValue('second_test_configurable'); $query = << Date: Tue, 20 Aug 2019 15:37:23 -0500 Subject: [PATCH 024/147] MC-18512: Dynamically inject all searchable custom attributes for product filtering --- app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php | 6 +----- app/code/Magento/CatalogGraphQl/etc/schema.graphqls | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php index 6a17f730c0f1a..3bc61a0fb3f2c 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php @@ -98,11 +98,7 @@ public function resolve( //get product children fields queried $productFields = (array)$info->getFieldSelection(1); - $searchCriteria = $this->searchApiCriteriaBuilder->build( - $args, - isset($productFields['filters']) - ); - + $searchCriteria = $this->searchApiCriteriaBuilder->build($args, isset($productFields['filters'])); $searchResult = $this->searchQuery->getResult($searchCriteria, $info, $args); if ($searchResult->getCurrentPage() > $searchResult->getTotalPages() && $searchResult->getTotalCount() > 0) { diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index e6daf4f4d753e..aadd43fafbd7f 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -221,7 +221,7 @@ interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model products( pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), - sort: ProductSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.") + sort: ProductAttributeSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.") ): CategoryProducts @doc(description: "The list of products assigned to the category.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Products") breadcrumbs: [Breadcrumb] @doc(description: "Breadcrumbs, parent categories info.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Breadcrumbs") } From 8d765a32dd7480b3a17d9745aeb2ee6d7f80bbe2 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Tue, 20 Aug 2019 16:53:20 -0500 Subject: [PATCH 025/147] MC-19441: Fix web api test failures - fixed 3 tests --- .../GraphQl/Catalog/ProductSearchTest.php | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index 3284a4a7172fe..bcd0ffedf542f 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -717,6 +717,7 @@ public function testFilterVisibleProductsWithMatchingSkuOrNameWithSpecialPrice() public function testSearchWithFilterWithPageSizeEqualTotalCount() { + CacheCleaner::cleanAll(); $query = << Date: Wed, 21 Aug 2019 15:18:16 -0500 Subject: [PATCH 026/147] MC-19541: Fix the filtering by price --- .../Product/SearchCriteriaBuilder.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php index 9e381307d8139..4872c627ded59 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -84,9 +84,11 @@ public function __construct( public function build(array $args, bool $includeAggregation): SearchCriteriaInterface { $searchCriteria = $this->builder->build('products', $args); + $this->updateRangeFilters($searchCriteria); $searchCriteria->setRequestName( $includeAggregation ? 'graphql_product_search_with_aggregation' : 'graphql_product_search' ); + if ($includeAggregation) { $this->preparePriceAggregation($searchCriteria); } @@ -175,4 +177,24 @@ private function addDefaultSortOrder(SearchCriteriaInterface $searchCriteria): v ->create(); $searchCriteria->setSortOrders([$sortOrder]); } + + /** + * Format range filters so replacement works + * + * Range filter fields in search request must replace value like '%field.from%' or '%field.to%' + * + * @param SearchCriteriaInterface $searchCriteria + */ + private function updateRangeFilters(SearchCriteriaInterface $searchCriteria): void + { + $filterGroups = $searchCriteria->getFilterGroups(); + foreach ($filterGroups as $filterGroup) { + $filters = $filterGroup->getFilters(); + foreach ($filters as $filter) { + if (in_array($filter->getConditionType(), ['from', 'to'])) { + $filter->setField($filter->getField() . '.' . $filter->getConditionType()); + } + } + } + } } From c1e34125e9bdb17ca3ab82ea9a422754c56e365d Mon Sep 17 00:00:00 2001 From: Deepty Thampy Date: Wed, 21 Aug 2019 18:19:45 -0500 Subject: [PATCH 027/147] MC-18514: API functional test to cover filterable custom attributes in layered navigation - fix failing tests and removing duplicates --- .../GraphQl/Catalog/ProductSearchTest.php | 123 +++++------------- .../GraphQl/Catalog/ProductViewTest.php | 2 +- .../GraphQl/VariablesSupportQueryTest.php | 6 +- 3 files changed, 36 insertions(+), 95 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index bcd0ffedf542f..a565ac656124a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -1110,60 +1110,6 @@ public function testQuerySortByPriceDESCWithDefaultPageSize() $this->assertEquals(1, $response['products']['page_info']['current_page']); } - /** - * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php - */ - public function testProductQueryUsingFromAndToFilterInput() - { - $query - = <<graphQlQuery($query); - $this->assertEquals(2, $response['products']['total_count']); - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - $product1 = $productRepository->get('simple1'); - $product2 = $productRepository->get('simple2'); - $filteredProducts = [$product2, $product1]; - - $this->assertProductItemsWithMaximalAndMinimalPriceCheck($filteredProducts, $response); - } - /** * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php */ @@ -1230,7 +1176,7 @@ public function testProductBasicFullTextSearchQuery() /** * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php */ - public function testProductsThatMatchWithPricesFromList() + public function testProductsThatMatchWithinASpecificPriceRange() { $query =<<get('simple2'); $prod2 = $productRepository->get('simple1'); $filteredProducts = [$prod1, $prod2]; - $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); - foreach ($productItemsInResponse as $itemIndex => $itemArray) { - $this->assertNotEmpty($itemArray); - $this->assertResponseFields( - $productItemsInResponse[$itemIndex][0], - ['attribute_set_id' => $filteredProducts[$itemIndex]->getAttributeSetId(), - 'sku' => $filteredProducts[$itemIndex]->getSku(), - 'name' => $filteredProducts[$itemIndex]->getName(), - 'price' => [ - 'regularPrice' => [ - 'amount' => [ - 'value' => $filteredProducts[$itemIndex]->getPrice(), - 'currency' => 'USD' - ] - ] - ], - 'type_id' =>$filteredProducts[$itemIndex]->getTypeId(), - 'weight' => $filteredProducts[$itemIndex]->getWeight() - ] - ); - } + $this->assertProductItemsWithPriceCheck($filteredProducts, $response); } /** @@ -1320,21 +1258,17 @@ public function testQueryFilterNoMatchingItems() { products( filter: - { - special_price:{lt:"15"} - price:{lt:"50"} - weight:{gt:"4"} - or: - { - sku:{like:"simple%"} - name:{like:"%simple%"} - } + { + price:{from:"50"} + sku:{like:"simple%"} + name:{like:"simple%"} + } pageSize:2 currentPage:1 sort: { - sku:ASC + position:ASC } ) { @@ -1384,7 +1318,7 @@ public function testQueryPageOutOfBoundException() products( filter: { - price:{eq:"10"} + price:{to:"10"} } pageSize:2 currentPage:2 @@ -1605,7 +1539,7 @@ private function assertProductItems(array $filteredProducts, array $actualRespon } } - private function assertProductItemsWithMaximalAndMinimalPriceCheck(array $filteredProducts, array $actualResponse) + private function assertProductItemsWithPriceCheck(array $filteredProducts, array $actualResponse) { $productItemsInResponse = array_map(null, $actualResponse['products']['items'], $filteredProducts); @@ -1628,7 +1562,14 @@ private function assertProductItemsWithMaximalAndMinimalPriceCheck(array $filter 'value' => $filteredProducts[$itemIndex]->getSpecialPrice(), 'currency' => 'USD' ] - ] + ], + 'regularPrice' => [ + 'amount' => [ + 'value' => $filteredProducts[$itemIndex]->getPrice(), + 'currency' => 'USD' + ] + ] + ], 'type_id' =>$filteredProducts[$itemIndex]->getTypeId(), 'weight' => $filteredProducts[$itemIndex]->getWeight() diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php index e11e2e8d108c2..5990211f1e47d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php @@ -592,7 +592,7 @@ public function testProductPrices() $secondProductSku = 'simple-156'; $query = << 1, 'priceSort' => 'ASC', 'filterInput' => [ - 'min_price' => [ - 'gt' => 150, + 'price' => [ + 'from' => 150, ], ], ]; From b2c8e78eead88e4b1cbd6d4135929f94811dbb21 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Thu, 22 Aug 2019 12:00:04 -0500 Subject: [PATCH 028/147] MC-19579: Multiple sort parameters does not work with ElasticSearch --- .../Magento/Framework/Search/Request/Builder.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/Search/Request/Builder.php b/lib/internal/Magento/Framework/Search/Request/Builder.php index 74bc65010a934..0cf959b657c76 100644 --- a/lib/internal/Magento/Framework/Search/Request/Builder.php +++ b/lib/internal/Magento/Framework/Search/Request/Builder.php @@ -6,6 +6,7 @@ namespace Magento\Framework\Search\Request; +use Magento\Framework\Api\SortOrder; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Phrase; use Magento\Framework\Search\RequestInterface; @@ -173,7 +174,13 @@ public function create() private function prepareSorts(array $sorts) { $sortData = []; - foreach ($sorts as $sortField => $direction) { + foreach ($sorts as $sortField => $sort) { + if ($sort instanceof SortOrder) { + $sortField = $sort->getField(); + $direction = $sort->getDirection(); + } else { + $direction = $sort; + } $sortData[] = [ 'field' => $sortField, 'direction' => $direction, From 8bba2b7f2d8c4edbdfd684c54174d5abfe64a55f Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Thu, 22 Aug 2019 12:19:19 -0500 Subject: [PATCH 029/147] MC-19572: Fix filtering by attribute of type select/multiselect using filter input type "in" --- .../Product/SearchCriteriaBuilder.php | 7 ++++--- .../Model/Config/FilterAttributeReader.php | 1 + .../ProductEntityAttributesForAst.php | 16 +++------------- .../CatalogGraphQl/etc/search_request.xml | 4 ++-- 4 files changed, 10 insertions(+), 18 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php index 4872c627ded59..1e646b3c0b74b 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -85,13 +85,14 @@ public function build(array $args, bool $includeAggregation): SearchCriteriaInte { $searchCriteria = $this->builder->build('products', $args); $this->updateRangeFilters($searchCriteria); - $searchCriteria->setRequestName( - $includeAggregation ? 'graphql_product_search_with_aggregation' : 'graphql_product_search' - ); if ($includeAggregation) { $this->preparePriceAggregation($searchCriteria); + $requestName = 'graphql_product_search_with_aggregation'; + } else { + $requestName = 'graphql_product_search'; } + $searchCriteria->setRequestName($requestName); if (!empty($args['search'])) { $this->addFilter($searchCriteria, 'search_term', $args['search']); diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php index 9310dc593d40c..b2cb4dca28bde 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php +++ b/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php @@ -97,6 +97,7 @@ private function getFilterType($attributeType): string 'price' => self::FILTER_RANGE_TYPE, 'date' => self::FILTER_RANGE_TYPE, 'select' => self::FILTER_EQUAL_TYPE, + 'multiselect' => self::FILTER_EQUAL_TYPE, 'boolean' => self::FILTER_EQUAL_TYPE, 'text' => self::FILTER_LIKE_TYPE, 'textarea' => self::FILTER_LIKE_TYPE, diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php index 17409210808cc..973b8fbcd6b0f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php @@ -29,26 +29,16 @@ class ProductEntityAttributesForAst implements FieldEntityAttributesInterface */ private $additionalAttributes = ['min_price', 'max_price', 'category_id']; - /** - * Array to translate graphql field to internal entity attribute - * - * @var array - */ - private $translatedAttributes = ['category_id' => 'category_ids']; - /** * @param ConfigInterface $config * @param string[] $additionalAttributes - * @param array $translatedAttributes */ public function __construct( ConfigInterface $config, - array $additionalAttributes = [], - array $translatedAttributes = [] + array $additionalAttributes = [] ) { $this->config = $config; $this->additionalAttributes = array_merge($this->additionalAttributes, $additionalAttributes); - $this->translatedAttributes = array_merge($this->translatedAttributes, $translatedAttributes); } /** @@ -74,7 +64,7 @@ public function getEntityAttributes() : array foreach ($configElement->getFields() as $field) { $fields[$field->getName()] = [ 'type' => 'String', - 'fieldName' => $this->translatedAttributes[$field->getName()] ?? $field->getName(), + 'fieldName' => $field->getName(), ]; } } @@ -82,7 +72,7 @@ public function getEntityAttributes() : array foreach ($this->additionalAttributes as $attributeName) { $fields[$attributeName] = [ 'type' => 'String', - 'fieldName' => $this->translatedAttributes[$attributeName] ?? $attributeName, + 'fieldName' => $attributeName, ]; } diff --git a/app/code/Magento/CatalogGraphQl/etc/search_request.xml b/app/code/Magento/CatalogGraphQl/etc/search_request.xml index 5e962d8467a4f..ab1eea9eb6fda 100644 --- a/app/code/Magento/CatalogGraphQl/etc/search_request.xml +++ b/app/code/Magento/CatalogGraphQl/etc/search_request.xml @@ -34,7 +34,7 @@ - + @@ -80,7 +80,7 @@ - + From 3ad13cd43259fb27354e33c5a94badf1b2f052eb Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Fri, 23 Aug 2019 13:54:21 -0500 Subject: [PATCH 030/147] MC-19572: Fix filtering by attribute of type select/multiselect using filter input type "in" --- .../Model/Adapter/Mysql/Filter/Preprocessor.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php index c758e773f43c1..37d2dda886259 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php @@ -165,7 +165,10 @@ private function processQueryWithField(FilterInterface $filter, $isNegation, $qu $this->customerSession->getCustomerGroupId() ); } elseif ($filter->getField() === 'category_ids') { - return 'category_ids_index.category_id = ' . (int) $filter->getValue(); + return $this->connection->quoteInto( + 'category_ids_index.category_id in (?)', + $filter->getValue() + ); } elseif ($attribute->isStatic()) { $alias = $this->aliasResolver->getAlias($filter); $resultQuery = str_replace( From 3f2f2bcc4ae99fb57c932125a7531e63c19e3136 Mon Sep 17 00:00:00 2001 From: Deepty Thampy Date: Mon, 26 Aug 2019 10:13:03 -0500 Subject: [PATCH 031/147] MC-19572: Fix filtering by attribute of type select/multiselect using filter input type in - added test and fixtures for multiselect --- .../GraphQl/Catalog/ProductSearchTest.php | 116 ++++++++++++++++-- ..._attribute_layered_navigation_rollback.php | 11 ++ ..._navigation_with_multiselect_attribute.php | 98 +++++++++++++++ ...on_with_multiselect_attribute_rollback.php | 32 +++++ 4 files changed, 246 insertions(+), 11 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute_rollback.php diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index a565ac656124a..6356f7b44db84 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -95,7 +95,7 @@ public function testFilterLn() } /** - * Advanced Search which uses product attribute to filter out the results + * Filter products using custom attribute of input type select(dropdown) and filterTypeInput eq * * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -105,7 +105,42 @@ public function testAdvancedSearchByOneCustomAttribute() CacheCleaner::cleanAll(); $attributeCode = 'second_test_configurable'; $optionValue = $this->getDefaultAttributeOptionValue($attributeCode); - $query = $this->getQueryProductsWithCustomAttribute($attributeCode, $optionValue); + $query = <<get(ProductRepositoryInterface::class); @@ -151,6 +186,67 @@ public function testAdvancedSearchByOneCustomAttribute() ] ); } + /** + * Filter products using custom attribute of input type select(dropdown) and filterTypeInput eq + * + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testFilterProductsByMultiSelectCustomAttribute() + { + CacheCleaner::cleanAll(); + $attributeCode = 'multiselect_attribute'; + /** @var \Magento\Eav\Model\Config $eavConfig */ + $eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); + $attribute = $eavConfig->getAttribute('catalog_product', $attributeCode); + /** @var AttributeOptionInterface[] $options */ + $options = $attribute->getOptions(); + array_shift($options); + $optionValues = array(); + for ($i = 0; $i < count($options); $i++) { + $optionValues[] = $options[$i]->getValue(); + } + $query = <<graphQlQuery($query); + $this->assertEquals(3, $response['products']['total_count']); + $this->assertNotEmpty($response['products']['filters']); + } /** * Get the option value for the custom attribute to be used in the graphql query @@ -462,7 +558,7 @@ public function testLayeredNavigationWithConfigurableChildrenOutOfStock() /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ $productRepository = Bootstrap::getObjectManager()->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); $outOfStockChildProduct = $productRepository->get('simple_30'); - // Set another child product from 2nd Configurable product with attribute option1 to OOS + // All child variations with this attribute are now set to Out of Stock $outOfStockChildProduct->setStockData( ['use_config_manage_stock' => 1, 'qty' => 0, @@ -784,18 +880,17 @@ public function testQueryProductsInCurrentPageSortedByPriceASC() products( filter: { - price:{gt: "5", lt: "50"} - or: - { - sku:{like:"simple%"} - name:{like:"simple%"} - } + price:{to :"50"} + sku:{like:"simple%"} + name:{like:"simple%"} + } pageSize:4 currentPage:1 sort: { price:ASC + name:ASC } ) { @@ -1062,7 +1157,7 @@ public function testQuerySortByPriceDESCWithDefaultPageSize() products( filter: { - sku:{like:"%simple%"} + sku:{like:"simple%"} } sort: { @@ -1109,7 +1204,6 @@ public function testQuerySortByPriceDESCWithDefaultPageSize() $this->assertEquals(20, $response['products']['page_info']['page_size']); $this->assertEquals(1, $response['products']['page_info']['current_page']); } - /** * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation_rollback.php index 49e2a8e88a1ac..2e4227eb35392 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation_rollback.php @@ -7,3 +7,14 @@ // phpcs:ignore Magento2.Security.IncludeFile require __DIR__ . '/../../ConfigurableProduct/_files/configurable_products_rollback.php'; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Eav\Api\AttributeRepositoryInterface; + +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); + +/** @var AttributeRepositoryInterface $attributeRepository */ +$attributeRepository = Bootstrap::getObjectManager()->get(AttributeRepositoryInterface::class); +/** @var \Magento\Eav\Api\Data\AttributeInterface $attribute */ +$attribute = $attributeRepository->get('catalog_product', 'test_configurable'); +$attributeRepository->delete($attribute); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php new file mode 100644 index 0000000000000..7d4f22e154030 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php @@ -0,0 +1,98 @@ +create( + \Magento\Catalog\Setup\CategorySetup::class +); + +/** @var $options \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection */ +$options = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection::class +); +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); + +/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +$attribute = $eavConfig->getAttribute('catalog_product', 'multiselect_attribute'); + +$eavConfig->clear(); +$attribute->setIsSearchable(1) + ->setIsVisibleInAdvancedSearch(1) + ->setIsFilterable(true) + ->setIsFilterableInSearch(true) + ->setIsVisibleOnFront(1); +/** @var AttributeRepositoryInterface $attributeRepository */ +$attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); +$attributeRepository->save($attribute); + +$options->setAttributeFilter($attribute->getId()); +$optionIds = $options->getAllIds(); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager()->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId($optionIds[0] * 10) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setWebsiteIds([1]) + ->setName('With Multiselect 1 and 2') + ->setSku('simple_ms_1') + ->setPrice(10) + ->setDescription('Hello " &" Bring the water bottle when you can!') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setMultiselectAttribute([$optionIds[1],$optionIds[2]]) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); +$productRepository->save($product); + +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId($optionIds[1] * 10) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setWebsiteIds([1]) + ->setName('With Multiselect 2 and 3') + ->setSku('simple_ms_2') + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setMultiselectAttribute([$optionIds[2], $optionIds[3]]) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); +$productRepository->save($product); + +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId($optionIds[2] * 10) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setWebsiteIds([1]) + ->setName('With Multiselect 1 and 3') + ->setSku('simple_ms_2') + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setMultiselectAttribute([$optionIds[2], $optionIds[3]]) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + +$productRepository->save($product); + +/** @var \Magento\Indexer\Model\Indexer\Collection $indexerCollection */ +$indexerCollection = Bootstrap::getObjectManager()->get(\Magento\Indexer\Model\Indexer\Collection::class); +$indexerCollection->load(); +/** @var \Magento\Indexer\Model\Indexer $indexer */ +foreach ($indexerCollection->getItems() as $indexer) { + $indexer->reindexAll(); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute_rollback.php new file mode 100644 index 0000000000000..eb8201f04e6cc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute_rollback.php @@ -0,0 +1,32 @@ +get('Magento\Framework\Registry'); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $productCollection \Magento\Catalog\Model\ResourceModel\Product */ +$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create('Magento\Catalog\Model\Product') + ->getCollection(); + +foreach ($productCollection as $product) { + $product->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +\Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(IndexerRegistry::class) + ->get(Magento\CatalogInventory\Model\Indexer\Stock\Processor::INDEXER_ID) + ->reindexAll(); From 8b2f00207dec18bc6fb22cb35fdd98850805b577 Mon Sep 17 00:00:00 2001 From: Raoul Rego Date: Mon, 26 Aug 2019 11:31:28 -0500 Subject: [PATCH 032/147] MC-17627: Dependency static test does not analyze content of phtml files - Fixed regular expression to include php echo syntax --- .../static/testsuite/Magento/Test/Integrity/DependencyTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php index 16ba295588b58..540fcc76349d1 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php @@ -301,7 +301,7 @@ protected function _getCleanedFileContents($fileType, $file) //Removing html $contentsWithoutHtml = ''; preg_replace_callback( - '~(<\?php\s+.*\?>)~sU', + '~(<\?(php|=)\s+.*\?>)~sU', function ($matches) use ($contents, &$contentsWithoutHtml) { $contentsWithoutHtml .= $matches[1]; return $contents; From bb996868319ae8efa47088c9f8f2b217c4727819 Mon Sep 17 00:00:00 2001 From: Deepty Thampy Date: Mon, 26 Aug 2019 13:59:22 -0500 Subject: [PATCH 033/147] MC-19441: Fix web api test failures - fix fixture and tests --- .../GraphQl/Catalog/ProductSearchTest.php | 286 ++++++------------ ..._attribute_layered_navigation_rollback.php | 11 - 2 files changed, 100 insertions(+), 197 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index 6356f7b44db84..cd7239d808b41 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -94,6 +94,73 @@ public function testFilterLn() ); } + /** + * Layered navigation for Configurable products with out of stock options + * Two configurable products each having two variations and one of the child products of one Configurable set to OOS + * + * @magentoApiDataFixture Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testLayeredNavigationWithConfigurableChildrenOutOfStock() + { + CacheCleaner::cleanAll(); + $attributeCode = 'test_configurable'; + /** @var \Magento\Eav\Model\Config $eavConfig */ + $eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); + $attribute = $eavConfig->getAttribute('catalog_product', $attributeCode); + /** @var AttributeOptionInterface[] $options */ + $options = $attribute->getOptions(); + array_shift($options); + $firstOption = $options[0]->getValue(); + $secondOption = $options[1]->getValue(); + $query = $this->getQueryProductsWithCustomAttribute($attributeCode, $firstOption); + $response = $this->graphQlQuery($query); + + // 1 product is returned since only one child product with attribute option1 from 1st Configurable product is OOS + $this->assertEquals(1, $response['products']['total_count']); + + // Custom attribute filter layer data + $this->assertResponseFields( + $response['products']['filters'][1], + [ + 'name' => $attribute->getDefaultFrontendLabel(), + 'request_var'=> $attribute->getAttributeCode(), + 'filter_items_count'=> 2, + 'filter_items' => [ + [ + 'label' => 'Option 1', + 'items_count' => 1, + 'value_string' => $firstOption, + '__typename' =>'LayerFilterItem' + ], + [ + 'label' => 'Option 2', + 'items_count' => 1, + 'value_string' => $secondOption, + '__typename' =>'LayerFilterItem' + ] + ], + ] + ); + + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $productRepository = Bootstrap::getObjectManager()->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $outOfStockChildProduct = $productRepository->get('simple_30'); + // All child variations with this attribute are now set to Out of Stock + $outOfStockChildProduct->setStockData( + ['use_config_manage_stock' => 1, + 'qty' => 0, + 'is_qty_decimal' => 0, + 'is_in_stock' => 0] + ); + $productRepository->save($outOfStockChildProduct); + $query = $this->getQueryProductsWithCustomAttribute($attributeCode, $firstOption); + $response = $this->graphQlQuery($query); + $this->assertEquals(0, $response['products']['total_count']); + $this->assertEmpty($response['products']['items']); + $this->assertEmpty($response['products']['filters']); + } + /** * Filter products using custom attribute of input type select(dropdown) and filterTypeInput eq * @@ -373,7 +440,7 @@ public function testFullTextSearchForProductAndFilterByCustomAttribute() } /** - * Filter by category_id and custom attribute + * Filter by single category and custom attribute * * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -507,72 +574,6 @@ private function getQueryProductsWithCustomAttribute($attributeCode, $optionValu QUERY; } - /** - * Layered navigation for Configurable products with out of stock options - * Two configurable products each having two variations and one of the child products of one Configurable set to OOS - * - * @magentoApiDataFixture Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testLayeredNavigationWithConfigurableChildrenOutOfStock() - { - $attributeCode = 'test_configurable'; - /** @var \Magento\Eav\Model\Config $eavConfig */ - $eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); - $attribute = $eavConfig->getAttribute('catalog_product', $attributeCode); - /** @var AttributeOptionInterface[] $options */ - $options = $attribute->getOptions(); - array_shift($options); - $firstOption = $options[0]->getValue(); - $secondOption = $options[1]->getValue(); - $query = $this->getQueryProductsWithCustomAttribute($attributeCode, $firstOption); - $response = $this->graphQlQuery($query); - - //1 product will be returned since only one child product with attribute option1 from 1st Configurable product is OOS - $this->assertEquals(1, $response['products']['total_count']); - - // Custom attribute filter layer data - $this->assertResponseFields( - $response['products']['filters'][1], - [ - 'name' => $attribute->getDefaultFrontendLabel(), - 'request_var'=> $attribute->getAttributeCode(), - 'filter_items_count'=> 2, - 'filter_items' => [ - [ - 'label' => 'Option 1', - 'items_count' => 1, - 'value_string' => $firstOption, - '__typename' =>'LayerFilterItem' - ], - [ - 'label' => 'Option 2', - 'items_count' => 1, - 'value_string' => $secondOption, - '__typename' =>'LayerFilterItem' - ] - ], - ] - ); - - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $productRepository = Bootstrap::getObjectManager()->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); - $outOfStockChildProduct = $productRepository->get('simple_30'); - // All child variations with this attribute are now set to Out of Stock - $outOfStockChildProduct->setStockData( - ['use_config_manage_stock' => 1, - 'qty' => 0, - 'is_qty_decimal' => 0, - 'is_in_stock' => 0] - ); - $productRepository->save($outOfStockChildProduct); - $query = $this->getQueryProductsWithCustomAttribute($attributeCode, $firstOption); - $response = $this->graphQlQuery($query); - $this->assertEquals(0, $response['products']['total_count']); - $this->assertEmpty($response['products']['items']); - $this->assertEmpty($response['products']['filters']); - } - /** * Get array with expected data for layered navigation filters * @@ -672,12 +673,9 @@ public function testFilterProductsWithinSpecificPriceRangeSortedByNameDesc() products( filter: { - price:{gt: "5", lt: "50"} - or: - { - sku:{like:"simple%"} - name:{like:"Simple%"} - } + price:{from: "5", to: "50"} + sku:{like:"simple%"} + name:{like:"Simple%"} } pageSize:4 currentPage:1 @@ -729,79 +727,6 @@ public function testFilterProductsWithinSpecificPriceRangeSortedByNameDesc() $this->assertEquals(4, $response['products']['page_info']['page_size']); } - /** - * Test a visible product with matching sku or name with special price - * - * Requesting for items that has a special price and price < $60, that are visible in Catalog, Search or Both which - * either has a sku like “simple” or name like “configurable”sorted by price in DESC - * - * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testFilterVisibleProductsWithMatchingSkuOrNameWithSpecialPrice() - { - $query - = <<get(ProductRepositoryInterface::class); - $product1 = $productRepository->get('simple1'); - $product2 = $productRepository->get('simple2'); - $filteredProducts = [$product2, $product1]; - - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('total_count', $response['products']); - $this->assertEquals(2, $response['products']['total_count']); - $this->assertProductItems($filteredProducts, $response); - } - /** * pageSize = total_count and current page = 2 * expected - error is thrown @@ -813,7 +738,7 @@ public function testFilterVisibleProductsWithMatchingSkuOrNameWithSpecialPrice() public function testSearchWithFilterWithPageSizeEqualTotalCount() { - CacheCleaner::cleanAll(); + $query = <<graphQlQuery($query); /** @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - /** @var ProductInterface $product */ - $product = $productRepository->get('simple333'); - $categoryIds = $product->getCategoryIds(); - foreach ($categoryIds as $index => $value) { - $categoryIds[$index] = [ 'id' => (int)$value]; - } - $this->assertNotEmpty($response['products']['items'][0]['categories'], "Categories must not be empty"); - $this->assertNotNull($response['products']['items'][0]['categories'], "categories must not be null"); - $this->assertEquals($categoryIds, $response['products']['items'][0]['categories']); - /** @var MetadataPool $metaData */ - $metaData = ObjectManager::getInstance()->get(MetadataPool::class); - $linkField = $metaData->getMetadata(ProductInterface::class)->getLinkField(); - $assertionMap = [ - - ['response_field' => 'id', 'expected_value' => $product->getData($linkField)], - ['response_field' => 'sku', 'expected_value' => $product->getSku()], - ['response_field' => 'name', 'expected_value' => $product->getName()], - ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()] - ]; - $this->assertResponseFields($response['products']['items'][0], $assertionMap); + $this->assertEquals(3, $response['products']['total_count']); } /** + * Filter products by category only + * * @magentoApiDataFixture Magento/Catalog/_files/product_in_multiple_categories.php * @return void */ @@ -1150,7 +1064,7 @@ public function testFilterProductsBySingleCategoryId() */ public function testQuerySortByPriceDESCWithDefaultPageSize() { - CacheCleaner::cleanAll(); + $query = <<get(\Magento\Eav\Model\Config::class); - -/** @var AttributeRepositoryInterface $attributeRepository */ -$attributeRepository = Bootstrap::getObjectManager()->get(AttributeRepositoryInterface::class); -/** @var \Magento\Eav\Api\Data\AttributeInterface $attribute */ -$attribute = $attributeRepository->get('catalog_product', 'test_configurable'); -$attributeRepository->delete($attribute); From 6730caed941cf5971f22bb7f1b151d4163622b5b Mon Sep 17 00:00:00 2001 From: Deepty Thampy Date: Mon, 26 Aug 2019 17:38:51 -0500 Subject: [PATCH 034/147] MC-19441: Fix web api test failures - fix fixture and tests --- .../testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index cd7239d808b41..169aba7c15734 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -746,7 +746,7 @@ public function testSearchWithFilterWithPageSizeEqualTotalCount() search : "simple" filter: { - price:{from:"60"} + price:{from:"5.59"} } pageSize:2 currentPage:2 From 211f2a3860562c73a9f65fbaeea2761411bda213 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Tue, 27 Aug 2019 09:28:07 -0500 Subject: [PATCH 035/147] MC-18450: Allow custom attributes to be filterable and also returned in the layered navigation the product filter section in GraphQL - Add relevance to sort options - Refactor and decouple searching and filtering --- .../Products/DataProvider/Product.php | 30 ++-- .../Product/CollectionPostProcessor.php | 42 +++++ .../Products/DataProvider/ProductSearch.php | 144 ++++++++++++++++++ .../Products/Query/FieldSelection.php | 93 +++++++++++ .../Model/Resolver/Products/Query/Filter.php | 69 +-------- .../Model/Resolver/Products/Query/Search.php | 88 +++++------ .../CatalogGraphQl/etc/schema.graphqls | 1 + 7 files changed, 339 insertions(+), 128 deletions(-) create mode 100644 app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionPostProcessor.php create mode 100644 app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php create mode 100644 app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php index e5e0d1aea4285..2076ec6726988 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php @@ -8,6 +8,7 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider; use Magento\Catalog\Model\Product\Visibility; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionPostProcessor; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory; @@ -32,7 +33,12 @@ class Product /** * @var CollectionProcessorInterface */ - private $collectionProcessor; + private $collectionPreProcessor; + + /** + * @var CollectionPostProcessor + */ + private $collectionPostProcessor; /** * @var Visibility @@ -44,17 +50,20 @@ class Product * @param ProductSearchResultsInterfaceFactory $searchResultsFactory * @param Visibility $visibility * @param CollectionProcessorInterface $collectionProcessor + * @param CollectionPostProcessor $collectionPostProcessor */ public function __construct( CollectionFactory $collectionFactory, ProductSearchResultsInterfaceFactory $searchResultsFactory, Visibility $visibility, - CollectionProcessorInterface $collectionProcessor + CollectionProcessorInterface $collectionProcessor, + CollectionPostProcessor $collectionPostProcessor ) { $this->collectionFactory = $collectionFactory; $this->searchResultsFactory = $searchResultsFactory; $this->visibility = $visibility; - $this->collectionProcessor = $collectionProcessor; + $this->collectionPreProcessor = $collectionProcessor; + $this->collectionPostProcessor = $collectionPostProcessor; } /** @@ -75,7 +84,7 @@ public function getList( /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */ $collection = $this->collectionFactory->create(); - $this->collectionProcessor->process($collection, $searchCriteria, $attributes); + $this->collectionPreProcessor->process($collection, $searchCriteria, $attributes); if (!$isChildSearch) { $visibilityIds = $isSearch @@ -83,18 +92,9 @@ public function getList( : $this->visibility->getVisibleInCatalogIds(); $collection->setVisibility($visibilityIds); } - $collection->load(); - // Methods that perform extra fetches post-load - if (in_array('media_gallery_entries', $attributes)) { - $collection->addMediaGalleryData(); - } - if (in_array('media_gallery', $attributes)) { - $collection->addMediaGalleryData(); - } - if (in_array('options', $attributes)) { - $collection->addOptionsToResult(); - } + $collection->load(); + $this->collectionPostProcessor->process($collection, $attributes); $searchResult = $this->searchResultsFactory->create(); $searchResult->setSearchCriteria($searchCriteria); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionPostProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionPostProcessor.php new file mode 100644 index 0000000000000..fadf22e7643af --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionPostProcessor.php @@ -0,0 +1,42 @@ +isLoaded()) { + $collection->load(); + } + // Methods that perform extra fetches post-load + if (in_array('media_gallery_entries', $attributeNames)) { + $collection->addMediaGalleryData(); + } + if (in_array('media_gallery', $attributeNames)) { + $collection->addMediaGalleryData(); + } + if (in_array('options', $attributeNames)) { + $collection->addOptionsToResult(); + } + + return $collection; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php new file mode 100644 index 0000000000000..8c5fe42c730f6 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -0,0 +1,144 @@ +collectionFactory = $collectionFactory; + $this->searchResultsFactory = $searchResultsFactory; + $this->collectionPreProcessor = $collectionPreProcessor; + $this->collectionPostProcessor = $collectionPostProcessor; + $this->searchResultApplierFactory = $searchResultsApplierFactory; + } + + /** + * Get list of product data with full data set. Adds eav attributes to result set from passed in array + * + * @param SearchCriteriaInterface $searchCriteria + * @param SearchResultInterface $searchResult + * @param array $attributes + * @return SearchResultsInterface + */ + public function getList( + SearchCriteriaInterface $searchCriteria, + SearchResultsInterface $searchResult, + array $attributes = [] + ): SearchResultsInterface { + /** @var Collection $collection */ + $collection = $this->collectionFactory->create(); + + //Join search results + $this->getSearchResultsApplier($searchResult, $collection, $this->getSortOrderArray($searchCriteria))->apply(); + + $this->collectionPreProcessor->process($collection, $searchCriteria, $attributes); + $collection->load(); + $this->collectionPostProcessor->process($collection, $attributes); + + $searchResult = $this->searchResultsFactory->create(); + $searchResult->setSearchCriteria($searchCriteria); + $searchResult->setItems($collection->getItems()); + $searchResult->setTotalCount($collection->getSize()); + return $searchResult; + } + + /** + * Create searchResultApplier + * + * @param SearchResultInterface $searchResult + * @param Collection $collection + * @param array $orders + * @return SearchResultApplierInterface + */ + private function getSearchResultsApplier( + SearchResultInterface $searchResult, + Collection $collection, + array $orders + ): SearchResultApplierInterface { + return $this->searchResultApplierFactory->create( + [ + 'collection' => $collection, + 'searchResult' => $searchResult, + 'orders' => $orders + ] + ); + } + + /** + * Format sort orders into associative array + * + * E.g. ['field1' => 'DESC', 'field2' => 'ASC", ...] + * + * @param SearchCriteriaInterface $searchCriteria + * @return array + */ + private function getSortOrderArray(SearchCriteriaInterface $searchCriteria) + { + $ordersArray = []; + $sortOrders = $searchCriteria->getSortOrders(); + if (is_array($sortOrders)) { + foreach ($sortOrders as $sortOrder) { + $ordersArray[$sortOrder->getField()] = $sortOrder->getDirection(); + } + } + + return $ordersArray; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php new file mode 100644 index 0000000000000..3912bab05ebbe --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php @@ -0,0 +1,93 @@ +fieldTranslator = $fieldTranslator; + } + + /** + * Get requested fields from products query + * + * @param ResolveInfo $resolveInfo + * @return string[] + */ + public function getProductsFieldSelection(ResolveInfo $resolveInfo): array + { + return $this->getProductFields($resolveInfo); + } + + /** + * Return field names for all requested product fields. + * + * @param ResolveInfo $info + * @return string[] + */ + private function getProductFields(ResolveInfo $info): array + { + $fieldNames = []; + foreach ($info->fieldNodes as $node) { + if ($node->name->value !== 'products') { + continue; + } + foreach ($node->selectionSet->selections as $selection) { + if ($selection->name->value !== 'items') { + continue; + } + $fieldNames[] = $this->collectProductFieldNames($selection, $fieldNames); + } + } + + $fieldNames = array_merge(...$fieldNames); + + return $fieldNames; + } + + /** + * Collect field names for each node in selection + * + * @param SelectionNode $selection + * @param array $fieldNames + * @return array + */ + private function collectProductFieldNames(SelectionNode $selection, array $fieldNames = []): array + { + foreach ($selection->selectionSet->selections as $itemSelection) { + if ($itemSelection->kind === 'InlineFragment') { + foreach ($itemSelection->selectionSet->selections as $inlineSelection) { + if ($inlineSelection->kind === 'InlineFragment') { + continue; + } + $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value); + } + continue; + } + $fieldNames[] = $this->fieldTranslator->translate($itemSelection->name->value); + } + + return $fieldNames; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php index 6771777341281..cc25af44fdfbe 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php @@ -7,13 +7,11 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\Query; -use GraphQL\Language\AST\SelectionNode; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResultFactory; -use Magento\Framework\GraphQl\Query\FieldTranslator; /** * Retrieve filtered product data based off given search criteria in a format that GraphQL can interpret. @@ -31,31 +29,31 @@ class Filter private $productDataProvider; /** - * @var FieldTranslator + * @var \Magento\Catalog\Model\Layer\Resolver */ - private $fieldTranslator; + private $layerResolver; /** - * @var \Magento\Catalog\Model\Layer\Resolver + * FieldSelection */ - private $layerResolver; + private $fieldSelection; /** * @param SearchResultFactory $searchResultFactory * @param Product $productDataProvider * @param \Magento\Catalog\Model\Layer\Resolver $layerResolver - * @param FieldTranslator $fieldTranslator + * @param FieldSelection $fieldSelection */ public function __construct( SearchResultFactory $searchResultFactory, Product $productDataProvider, \Magento\Catalog\Model\Layer\Resolver $layerResolver, - FieldTranslator $fieldTranslator + FieldSelection $fieldSelection ) { $this->searchResultFactory = $searchResultFactory; $this->productDataProvider = $productDataProvider; - $this->fieldTranslator = $fieldTranslator; $this->layerResolver = $layerResolver; + $this->fieldSelection = $fieldSelection; } /** @@ -71,7 +69,7 @@ public function getResult( ResolveInfo $info, bool $isSearch = false ): SearchResult { - $fields = $this->getProductFields($info); + $fields = $this->fieldSelection->getProductsFieldSelection($info); $products = $this->productDataProvider->getList($searchCriteria, $fields, $isSearch); $productArray = []; /** @var \Magento\Catalog\Model\Product $product */ @@ -87,55 +85,4 @@ public function getResult( ] ); } - - /** - * Return field names for all requested product fields. - * - * @param ResolveInfo $info - * @return string[] - */ - private function getProductFields(ResolveInfo $info) : array - { - $fieldNames = []; - foreach ($info->fieldNodes as $node) { - if ($node->name->value !== 'products') { - continue; - } - foreach ($node->selectionSet->selections as $selection) { - if ($selection->name->value !== 'items') { - continue; - } - $fieldNames[] = $this->collectProductFieldNames($selection, $fieldNames); - } - } - - $fieldNames = array_merge(...$fieldNames); - - return $fieldNames; - } - - /** - * Collect field names for each node in selection - * - * @param SelectionNode $selection - * @param array $fieldNames - * @return array - */ - private function collectProductFieldNames(SelectionNode $selection, array $fieldNames = []): array - { - foreach ($selection->selectionSet->selections as $itemSelection) { - if ($itemSelection->kind === 'InlineFragment') { - foreach ($itemSelection->selectionSet->selections as $inlineSelection) { - if ($inlineSelection->kind === 'InlineFragment') { - continue; - } - $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value); - } - continue; - } - $fieldNames[] = $this->fieldTranslator->translate($itemSelection->name->value); - } - - return $fieldNames; - } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index c7dd2a8b05b79..a6d7f8641bd86 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -7,9 +7,9 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\Query; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ProductSearch; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\Api\Search\SearchCriteriaInterface; -use Magento\CatalogGraphQl\Model\Resolver\Products\SearchCriteria\Helper\Filter as FilterHelper; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResultFactory; use Magento\Search\Api\SearchInterface; @@ -25,26 +25,11 @@ class Search */ private $search; - /** - * @var FilterHelper - */ - private $filterHelper; - - /** - * @var Filter - */ - private $filterQuery; - /** * @var SearchResultFactory */ private $searchResultFactory; - /** - * @var \Magento\Framework\EntityManager\MetadataPool - */ - private $metadataPool; - /** * @var \Magento\Search\Model\Search\PageSizeProvider */ @@ -55,31 +40,38 @@ class Search */ private $searchCriteriaFactory; + /** + * @var FieldSelection + */ + private $fieldSelection; + + /** + * @var ProductSearch + */ + private $productsProvider; + /** * @param SearchInterface $search - * @param FilterHelper $filterHelper - * @param Filter $filterQuery * @param SearchResultFactory $searchResultFactory - * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool * @param \Magento\Search\Model\Search\PageSizeProvider $pageSize * @param SearchCriteriaInterfaceFactory $searchCriteriaFactory + * @param FieldSelection $fieldSelection + * @param ProductSearch $productsProvider */ public function __construct( SearchInterface $search, - FilterHelper $filterHelper, - Filter $filterQuery, SearchResultFactory $searchResultFactory, - \Magento\Framework\EntityManager\MetadataPool $metadataPool, \Magento\Search\Model\Search\PageSizeProvider $pageSize, - SearchCriteriaInterfaceFactory $searchCriteriaFactory + SearchCriteriaInterfaceFactory $searchCriteriaFactory, + FieldSelection $fieldSelection, + ProductSearch $productsProvider ) { $this->search = $search; - $this->filterHelper = $filterHelper; - $this->filterQuery = $filterQuery; $this->searchResultFactory = $searchResultFactory; - $this->metadataPool = $metadataPool; $this->pageSizeProvider = $pageSize; $this->searchCriteriaFactory = $searchCriteriaFactory; + $this->fieldSelection = $fieldSelection; + $this->productsProvider = $productsProvider; } /** @@ -87,20 +79,15 @@ public function __construct( * * @param SearchCriteriaInterface $searchCriteria * @param ResolveInfo $info - * @param array $args * @return SearchResult * @throws \Exception */ public function getResult( SearchCriteriaInterface $searchCriteria, - ResolveInfo $info, - array $args = [] + ResolveInfo $info ): SearchResult { - $idField = $this->metadataPool->getMetadata( - \Magento\Catalog\Api\Data\ProductInterface::class - )->getIdentifierField(); + $queryFields = $this->fieldSelection->getProductsFieldSelection($info); - $isSearch = isset($args['search']); $realPageSize = $searchCriteria->getPageSize(); $realCurrentPage = $searchCriteria->getCurrentPage(); // Current page must be set to 0 and page size to max for search to grab all ID's as temporary workaround @@ -109,35 +96,32 @@ public function getResult( $searchCriteria->setCurrentPage(0); $itemsResults = $this->search->search($searchCriteria); - $ids = []; - $searchIds = []; - foreach ($itemsResults->getItems() as $item) { - $ids[$item->getId()] = null; - $searchIds[] = $item->getId(); - } - - $searchCriteriaIds = $this->searchCriteriaFactory->create(); - $filter = $this->filterHelper->generate($idField, 'in', $searchIds); - $searchCriteriaIds = $this->filterHelper->add($searchCriteriaIds, $filter); - $searchCriteriaIds->setSortOrders($searchCriteria->getSortOrders()); - $searchCriteriaIds->setPageSize($realPageSize); - $searchCriteriaIds->setCurrentPage($realCurrentPage); + //Create copy of search criteria without conditions (conditions will be applied by joining search result) + $searchCriteriaCopy = $this->searchCriteriaFactory->create() + ->setSortOrders($searchCriteria->getSortOrders()) + ->setPageSize($realPageSize) + ->setCurrentPage($realCurrentPage); - $searchResult = $this->filterQuery->getResult($searchCriteriaIds, $info, $isSearch); + $searchResults = $this->productsProvider->getList($searchCriteriaCopy, $itemsResults, $queryFields); - $searchCriteria->setPageSize($realPageSize); - $searchCriteria->setCurrentPage($realCurrentPage); //possible division by 0 if ($realPageSize) { - $maxPages = (int)ceil($searchResult->getTotalCount() / $realPageSize); + $maxPages = (int)ceil($searchResults->getTotalCount() / $realPageSize); } else { $maxPages = 0; } + $productArray = []; + /** @var \Magento\Catalog\Model\Product $product */ + foreach ($searchResults->getItems() as $product) { + $productArray[$product->getId()] = $product->getData(); + $productArray[$product->getId()]['model'] = $product; + } + return $this->searchResultFactory->create( [ - 'totalCount' => $searchResult->getTotalCount(), - 'productsSearchResult' => $searchResult->getProductsSearchResult(), + 'totalCount' => $searchResults->getTotalCount(), + 'productsSearchResult' => $productArray, 'searchAggregation' => $itemsResults->getAggregations(), 'pageSize' => $realPageSize, 'currentPage' => $realCurrentPage, diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index aadd43fafbd7f..9d8b6f69f5842 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -374,6 +374,7 @@ input ProductSortInput @deprecated(reason: "Attributes used in this input are ha input ProductAttributeSortInput @doc(description: "ProductAttributeSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order. It's possible to sort products using searchable attributes with enabled 'Use in Filter Options' option") { position: SortEnum @doc(description: "The position of products") + relevance: SortEnum @doc(description: "The search relevance score (default)") } type MediaGalleryEntry @doc(description: "MediaGalleryEntry defines characteristics about images and videos associated with a specific product.") { From 4d6a89c6b97deb4e21c23a944288004a367505bc Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Tue, 27 Aug 2019 10:10:49 -0500 Subject: [PATCH 036/147] MC-19572: Fix filtering by attribute of type select/multiselect using filter input type in - fix unit test --- .../Adapter/Mysql/Filter/PreprocessorTest.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php index 7e3de7534e8c4..a79ffcc33cabe 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php @@ -129,7 +129,7 @@ protected function setUp() ->getMock(); $this->connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) ->disableOriginalConstructor() - ->setMethods(['select', 'getIfNullSql', 'quote']) + ->setMethods(['select', 'getIfNullSql', 'quote', 'quoteInto']) ->getMockForAbstractClass(); $this->select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) ->disableOriginalConstructor() @@ -222,9 +222,10 @@ public function testProcessPrice() public function processCategoryIdsDataProvider() { return [ - ['5', 'category_ids_index.category_id = 5'], - [3, 'category_ids_index.category_id = 3'], - ["' and 1 = 0", 'category_ids_index.category_id = 0'], + ['5', "category_ids_index.category_id in ('5')"], + [3, "category_ids_index.category_id in (3)"], + ["' and 1 = 0", "category_ids_index.category_id in ('\' and 1 = 0')"], + [['5', '10'], "category_ids_index.category_id in ('5', '10')"] ]; } @@ -251,6 +252,12 @@ public function testProcessCategoryIds($categoryId, $expectedResult) ->with(\Magento\Catalog\Model\Product::ENTITY, 'category_ids') ->will($this->returnValue($this->attribute)); + $this->connection + ->expects($this->once()) + ->method('quoteInto') + ->with('category_ids_index.category_id in (?)', $categoryId) + ->willReturn($expectedResult); + $actualResult = $this->target->process($this->filter, $isNegation, $query); $this->assertSame($expectedResult, $this->removeWhitespaces($actualResult)); } From 865566d79282333cc6f5c75f2730b7ffe032e35f Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Tue, 27 Aug 2019 12:05:42 -0500 Subject: [PATCH 037/147] MC-19618: Update schema for layered navigation output - Schema changes, implementation and static fixes --- .../Category/Query/CategoryAttributeQuery.php | 6 +- .../DataProvider/CategoryAttributesMapper.php | 15 ++-- .../LayeredNavigation/Builder/Attribute.php | 58 +++++----------- .../LayeredNavigation/Builder/Category.php | 49 ++++--------- .../Builder/Formatter/LayerFormatter.php | 48 +++++++++++++ .../LayeredNavigation/Builder/Price.php | 53 +++++---------- .../Model/Resolver/Aggregations.php | 68 +++++++++++++++++++ .../Model/Resolver/LayerFilters.php | 25 +------ .../CatalogGraphQl/etc/schema.graphqls | 16 ++++- 9 files changed, 194 insertions(+), 144 deletions(-) create mode 100644 app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Formatter/LayerFormatter.php create mode 100644 app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Category/Query/CategoryAttributeQuery.php b/app/code/Magento/CatalogGraphQl/DataProvider/Category/Query/CategoryAttributeQuery.php index 0b796c1457254..e3dfa38c78258 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Category/Query/CategoryAttributeQuery.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Category/Query/CategoryAttributeQuery.php @@ -49,9 +49,11 @@ public function getQuery(array $categoryIds, array $categoryAttributes, int $sto { $categoryAttributes = \array_merge($categoryAttributes, self::$requiredAttributes); - $attributeQuery = $this->attributeQueryFactory->create([ + $attributeQuery = $this->attributeQueryFactory->create( + [ 'entityType' => CategoryInterface::class - ]); + ] + ); return $attributeQuery->getQuery($categoryIds, $categoryAttributes, $storeId); } diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/CategoryAttributesMapper.php b/app/code/Magento/CatalogGraphQl/DataProvider/CategoryAttributesMapper.php index 1f8aa38d5b939..ea3c0b608d212 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/CategoryAttributesMapper.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/CategoryAttributesMapper.php @@ -65,12 +65,15 @@ private function formatAttributes(array $attributes): array $arrayTypeAttributes = $this->getFieldsOfArrayType(); return $arrayTypeAttributes - ? array_map(function ($data) use ($arrayTypeAttributes) { - foreach ($arrayTypeAttributes as $attributeCode) { - $data[$attributeCode] = $this->valueToArray($data[$attributeCode] ?? null); - } - return $data; - }, $attributes) + ? array_map( + function ($data) use ($arrayTypeAttributes) { + foreach ($arrayTypeAttributes as $attributeCode) { + $data[$attributeCode] = $this->valueToArray($data[$attributeCode] ?? null); + } + return $data; + }, + $attributes + ) : $attributes; } diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php index 82d167e323fc8..89df1be8d59f2 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php @@ -12,6 +12,7 @@ use Magento\Framework\Api\Search\AggregationInterface; use Magento\Framework\Api\Search\AggregationValueInterface; use Magento\Framework\Api\Search\BucketInterface; +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Formatter\LayerFormatter; /** * @inheritdoc @@ -35,6 +36,11 @@ class Attribute implements LayerBuilderInterface */ private $attributeOptionProvider; + /** + * @var LayerFormatter + */ + private $layerFormatter; + /** * @var array */ @@ -45,13 +51,16 @@ class Attribute implements LayerBuilderInterface /** * @param AttributeOptionProvider $attributeOptionProvider - * @param array $bucketNameFilter List with buckets name to be removed from filter + * @param LayerFormatter $layerFormatter + * @param array $bucketNameFilter */ public function __construct( AttributeOptionProvider $attributeOptionProvider, + LayerFormatter $layerFormatter, $bucketNameFilter = [] ) { $this->attributeOptionProvider = $attributeOptionProvider; + $this->layerFormatter = $layerFormatter; $this->bucketNameFilter = \array_merge($this->bucketNameFilter, $bucketNameFilter); } @@ -71,7 +80,7 @@ public function build(AggregationInterface $aggregation, ?int $storeId): array $attributeCode = \preg_replace('~_bucket$~', '', $bucketName); $attribute = $attributeOptions[$attributeCode] ?? []; - $result[$bucketName] = $this->buildLayer( + $result[$bucketName] = $this->layerFormatter->buildLayer( $attribute['attribute_label'] ?? $bucketName, \count($bucket->getValues()), $attribute['attribute_code'] ?? $bucketName @@ -79,7 +88,7 @@ public function build(AggregationInterface $aggregation, ?int $storeId): array foreach ($bucket->getValues() as $value) { $metrics = $value->getMetrics(); - $result[$bucketName]['filter_items'][] = $this->buildItem( + $result[$bucketName]['options'][] = $this->layerFormatter->buildItem( $attribute['options'][$metrics['value']] ?? $metrics['value'], $metrics['value'], $metrics['count'] @@ -109,40 +118,6 @@ private function getAttributeBuckets(AggregationInterface $aggregation) } } - /** - * Format layer data - * - * @param string $layerName - * @param string $itemsCount - * @param string $requestName - * @return array - */ - private function buildLayer($layerName, $itemsCount, $requestName): array - { - return [ - 'name' => $layerName, - 'filter_items_count' => $itemsCount, - 'request_var' => $requestName - ]; - } - - /** - * Format layer item data - * - * @param string $label - * @param string|int $value - * @param string|int $count - * @return array - */ - private function buildItem($label, $value, $count): array - { - return [ - 'label' => $label, - 'value_string' => $value, - 'items_count' => $count, - ]; - } - /** * Check that bucket contains data * @@ -165,9 +140,12 @@ private function getAttributeOptions(AggregationInterface $aggregation): array { $attributeOptionIds = []; foreach ($this->getAttributeBuckets($aggregation) as $bucket) { - $attributeOptionIds[] = \array_map(function (AggregationValueInterface $value) { - return $value->getValue(); - }, $bucket->getValues()); + $attributeOptionIds[] = \array_map( + function (AggregationValueInterface $value) { + return $value->getValue(); + }, + $bucket->getValues() + ); } if (!$attributeOptionIds) { diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php index c726f66e8c926..97644328abc2a 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php @@ -15,6 +15,7 @@ use Magento\Framework\Api\Search\AggregationValueInterface; use Magento\Framework\Api\Search\BucketInterface; use Magento\Framework\App\ResourceConnection; +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Formatter\LayerFormatter; /** * @inheritdoc @@ -56,22 +57,30 @@ class Category implements LayerBuilderInterface */ private $rootCategoryProvider; + /** + * @var LayerFormatter + */ + private $layerFormatter; + /** * @param CategoryAttributeQuery $categoryAttributeQuery * @param CategoryAttributesMapper $attributesMapper * @param RootCategoryProvider $rootCategoryProvider * @param ResourceConnection $resourceConnection + * @param LayerFormatter $layerFormatter */ public function __construct( CategoryAttributeQuery $categoryAttributeQuery, CategoryAttributesMapper $attributesMapper, RootCategoryProvider $rootCategoryProvider, - ResourceConnection $resourceConnection + ResourceConnection $resourceConnection, + LayerFormatter $layerFormatter ) { $this->categoryAttributeQuery = $categoryAttributeQuery; $this->attributesMapper = $attributesMapper; $this->resourceConnection = $resourceConnection; $this->rootCategoryProvider = $rootCategoryProvider; + $this->layerFormatter = $layerFormatter; } /** @@ -105,7 +114,7 @@ public function build(AggregationInterface $aggregation, ?int $storeId): array return []; } - $result = $this->buildLayer( + $result = $this->layerFormatter->buildLayer( self::$bucketMap[self::CATEGORY_BUCKET]['label'], \count($categoryIds), self::$bucketMap[self::CATEGORY_BUCKET]['request_name'] @@ -116,7 +125,7 @@ public function build(AggregationInterface $aggregation, ?int $storeId): array if (!\in_array($categoryId, $categoryIds, true)) { continue ; } - $result['filter_items'][] = $this->buildItem( + $result['options'][] = $this->layerFormatter->buildItem( $categoryLabels[$categoryId] ?? $categoryId, $categoryId, $value->getMetrics()['count'] @@ -126,40 +135,6 @@ public function build(AggregationInterface $aggregation, ?int $storeId): array return [$result]; } - /** - * Format layer data - * - * @param string $layerName - * @param string $itemsCount - * @param string $requestName - * @return array - */ - private function buildLayer($layerName, $itemsCount, $requestName): array - { - return [ - 'name' => $layerName, - 'filter_items_count' => $itemsCount, - 'request_var' => $requestName - ]; - } - - /** - * Format layer item data - * - * @param string $label - * @param string|int $value - * @param string|int $count - * @return array - */ - private function buildItem($label, $value, $count): array - { - return [ - 'label' => $label, - 'value_string' => $value, - 'items_count' => $count, - ]; - } - /** * Check that bucket contains data * diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Formatter/LayerFormatter.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Formatter/LayerFormatter.php new file mode 100644 index 0000000000000..72495f8ef8524 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Formatter/LayerFormatter.php @@ -0,0 +1,48 @@ + $layerName, + 'count' => $itemsCount, + 'attribute_code' => $requestName + ]; + } + + /** + * Format layer item data + * + * @param string $label + * @param string|int $value + * @param string|int $count + * @return array + */ + public function buildItem($label, $value, $count): array + { + return [ + 'label' => $label, + 'value' => $value, + 'count' => $count, + ]; + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php index 77f44afb4f679..e84a8f1b11078 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php @@ -10,6 +10,7 @@ use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\LayerBuilderInterface; use Magento\Framework\Api\Search\AggregationInterface; use Magento\Framework\Api\Search\BucketInterface; +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Formatter\LayerFormatter; /** * @inheritdoc @@ -21,6 +22,11 @@ class Price implements LayerBuilderInterface */ private const PRICE_BUCKET = 'price_bucket'; + /** + * @var LayerFormatter + */ + private $layerFormatter; + /** * @var array */ @@ -31,6 +37,15 @@ class Price implements LayerBuilderInterface ], ]; + /** + * @param LayerFormatter $layerFormatter + */ + public function __construct( + LayerFormatter $layerFormatter + ) { + $this->layerFormatter = $layerFormatter; + } + /** * @inheritdoc * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -42,7 +57,7 @@ public function build(AggregationInterface $aggregation, ?int $storeId): array return []; } - $result = $this->buildLayer( + $result = $this->layerFormatter->buildLayer( self::$bucketMap[self::PRICE_BUCKET]['label'], \count($bucket->getValues()), self::$bucketMap[self::PRICE_BUCKET]['request_name'] @@ -50,7 +65,7 @@ public function build(AggregationInterface $aggregation, ?int $storeId): array foreach ($bucket->getValues() as $value) { $metrics = $value->getMetrics(); - $result['filter_items'][] = $this->buildItem( + $result['options'][] = $this->layerFormatter->buildItem( \str_replace('_', '-', $metrics['value']), $metrics['value'], $metrics['count'] @@ -60,40 +75,6 @@ public function build(AggregationInterface $aggregation, ?int $storeId): array return [$result]; } - /** - * Format layer data - * - * @param string $layerName - * @param string $itemsCount - * @param string $requestName - * @return array - */ - private function buildLayer($layerName, $itemsCount, $requestName): array - { - return [ - 'name' => $layerName, - 'filter_items_count' => $itemsCount, - 'request_var' => $requestName - ]; - } - - /** - * Format layer item data - * - * @param string $label - * @param string|int $value - * @param string|int $count - * @return array - */ - private function buildItem($label, $value, $count): array - { - return [ - 'label' => $label, - 'value_string' => $value, - 'items_count' => $count, - ]; - } - /** * Check that bucket contains data * diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php new file mode 100644 index 0000000000000..47a1d1f977f9b --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php @@ -0,0 +1,68 @@ +filtersDataProvider = $filtersDataProvider; + $this->layerBuilder = $layerBuilder; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['layer_type']) || !isset($value['search_result'])) { + return null; + } + + $aggregations = $value['search_result']->getSearchAggregation(); + + if ($aggregations) { + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + $storeId = (int)$store->getId(); + return $this->layerBuilder->build($aggregations, $storeId); + } else { + return []; + } + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php index 6ef4e72627e82..0ec7e12e42d55 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php @@ -10,8 +10,6 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\LayerBuilder; -use Magento\Store\Api\Data\StoreInterface; /** * Layered navigation filters resolver, used for GraphQL request processing. @@ -23,21 +21,13 @@ class LayerFilters implements ResolverInterface */ private $filtersDataProvider; - /** - * @var LayerBuilder - */ - private $layerBuilder; - /** * @param \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider - * @param LayerBuilder $layerBuilder */ public function __construct( - \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider, - LayerBuilder $layerBuilder + \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider ) { $this->filtersDataProvider = $filtersDataProvider; - $this->layerBuilder = $layerBuilder; } /** @@ -50,19 +40,10 @@ public function resolve( array $value = null, array $args = null ) { - if (!isset($value['layer_type']) || !isset($value['search_result'])) { + if (!isset($value['layer_type'])) { return null; } - $aggregations = $value['search_result']->getSearchAggregation(); - - if ($aggregations) { - /** @var StoreInterface $store */ - $store = $context->getExtensionAttributes()->getStore(); - $storeId = (int)$store->getId(); - return $this->layerBuilder->build($aggregations, $storeId); - } else { - return []; - } + return $this->filtersDataProvider->getData($value['layer_type']); } } diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index aadd43fafbd7f..8900eb869d268 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -270,7 +270,8 @@ type Products @doc(description: "The Products object is the top-level object ret items: [ProductInterface] @doc(description: "An array of products that match the specified search criteria.") page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query.") total_count: Int @doc(description: "The number of products returned.") - filters: [LayerFilter] @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\LayerFilters") @doc(description: "Layered navigation filters array.") + filters: [LayerFilter] @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\LayerFilters") @doc(description: "Layered navigation filters array.") @deprecated(reason: "Use aggregations instead") + aggregations: [Aggregation] @doc(description: "Layered navigation aggregations.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Aggregations") sort_fields: SortFields @doc(description: "An object that includes the default sort field and all available sort fields.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\SortFields") } @@ -401,6 +402,19 @@ interface LayerFilterItemInterface @typeResolver(class: "Magento\\CatalogGraphQl items_count: Int @doc(description: "Count of items by filter.") } +type Aggregation { + count: Int @doc(description: "Count of filter items in filter group.") + label: String @doc(description: "Layered navigation filter name.") + attribute_code: String! @doc(description: "Attribute code of the filter item.") + options: [AggregationOption] @doc(description: "Array of aggregation options.") +} + +type AggregationOption { + count: Int @doc(description: "Count of items by filter.") + label: String! @doc(description: "Filter label.") + value: String! @doc(description: "Value for filter request variable to be used in query.") +} + type LayerFilterItem implements LayerFilterItemInterface { } From c4025da024cbdf7b2f9fa694caee9171d6e71bd1 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Tue, 27 Aug 2019 12:49:19 -0500 Subject: [PATCH 038/147] MC-19618: Update schema for layered navigation output - static fixes --- app/code/Magento/CatalogGraphQl/composer.json | 1 + .../Magento/GraphQl/Catalog/ProductSearchTest.php | 14 ++++++++------ ...ts_with_layered_navigation_custom_attribute.php | 3 ++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/composer.json b/app/code/Magento/CatalogGraphQl/composer.json index 13fcbe9a7d357..1582f29c25951 100644 --- a/app/code/Magento/CatalogGraphQl/composer.json +++ b/app/code/Magento/CatalogGraphQl/composer.json @@ -10,6 +10,7 @@ "magento/module-search": "*", "magento/module-store": "*", "magento/module-eav-graph-ql": "*", + "magento/module-catalog-search": "*", "magento/framework": "*" }, "suggest": { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index 169aba7c15734..990e2e0f31ac3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -116,7 +116,8 @@ public function testLayeredNavigationWithConfigurableChildrenOutOfStock() $query = $this->getQueryProductsWithCustomAttribute($attributeCode, $firstOption); $response = $this->graphQlQuery($query); - // 1 product is returned since only one child product with attribute option1 from 1st Configurable product is OOS + // 1 product is returned since only one child product with + // attribute option1 from 1st Configurable product is OOS $this->assertEquals(1, $response['products']['total_count']); // Custom attribute filter layer data @@ -269,8 +270,9 @@ public function testFilterProductsByMultiSelectCustomAttribute() /** @var AttributeOptionInterface[] $options */ $options = $attribute->getOptions(); array_shift($options); - $optionValues = array(); - for ($i = 0; $i < count($options); $i++) { + $optionValues = []; + $count = count($options); + for ($i = 0; $i < $count; $i++) { $optionValues[] = $options[$i]->getValue(); } $query = <<assertEquals(2, $response['products']['total_count']); $actualCategoryFilterItems = $response['products']['filters'][1]['filter_items']; //Validate the number of categories/sub-categories that contain the products with the custom attribute - $this->assertCount(6,$actualCategoryFilterItems); + $this->assertCount(6, $actualCategoryFilterItems); $expectedCategoryFilterItems = [ @@ -527,7 +529,7 @@ public function testFilterByCategoryIdAndCustomAttribute() $categoryFilterItems[$index][0]['items_count'], $actualCategoryFilterItems[$index]['items_count'], 'Products count in the category is incorrect' - ) ; + ); } } /** diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php index 6cd2819c2d296..864a931359bdd 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php @@ -115,7 +115,8 @@ 'catalog_product', $attributeSet->getId(), $attributeSet->getDefaultGroupId(), - $attribute1->getId()); + $attribute1->getId() + ); } $eavConfig->clear(); From f98027b62c79394dbb1e5aa0cd44ec26e637c09a Mon Sep 17 00:00:00 2001 From: Deepty Thampy Date: Tue, 27 Aug 2019 23:24:30 -0500 Subject: [PATCH 039/147] MC-18514: API functional test to cover filterable custom attributes in layered navigation - fix static failures tests and added fixture for sort by relevance --- .../GraphQl/Catalog/ProductSearchTest.php | 104 +++++++++++++++--- .../GraphQl/Catalog/ProductViewTest.php | 3 +- .../_files/products_for_relevance_sorting.php | 75 +++++++++++++ ...th_layered_navigation_custom_attribute.php | 3 +- 4 files changed, 169 insertions(+), 16 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting.php diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index 169aba7c15734..b6c02f0161ae2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -116,7 +116,7 @@ public function testLayeredNavigationWithConfigurableChildrenOutOfStock() $query = $this->getQueryProductsWithCustomAttribute($attributeCode, $firstOption); $response = $this->graphQlQuery($query); - // 1 product is returned since only one child product with attribute option1 from 1st Configurable product is OOS + // Out of two children, only one child product of 1st Configurable product with option1 is OOS $this->assertEquals(1, $response['products']['total_count']); // Custom attribute filter layer data @@ -269,7 +269,8 @@ public function testFilterProductsByMultiSelectCustomAttribute() /** @var AttributeOptionInterface[] $options */ $options = $attribute->getOptions(); array_shift($options); - $optionValues = array(); + $optionValues = []; + // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall for ($i = 0; $i < count($options); $i++) { $optionValues[] = $options[$i]->getValue(); } @@ -411,7 +412,7 @@ public function testFullTextSearchForProductAndFilterByCustomAttribute() $layers[$layerIndex][0]['request_var'], $response['products']['filters'][$layerIndex]['request_var'], 'request_var does not match' - ) ; + ); } // Validate the price filter layer data from the response @@ -490,7 +491,7 @@ public function testFilterByCategoryIdAndCustomAttribute() $this->assertEquals(2, $response['products']['total_count']); $actualCategoryFilterItems = $response['products']['filters'][1]['filter_items']; //Validate the number of categories/sub-categories that contain the products with the custom attribute - $this->assertCount(6,$actualCategoryFilterItems); + $this->assertCount(6, $actualCategoryFilterItems); $expectedCategoryFilterItems = [ @@ -527,7 +528,7 @@ public function testFilterByCategoryIdAndCustomAttribute() $categoryFilterItems[$index][0]['items_count'], $actualCategoryFilterItems[$index]['items_count'], 'Products count in the category is incorrect' - ) ; + ); } } /** @@ -972,7 +973,7 @@ public function testFilteringForProductsFromMultipleCategories() } /** - * Filter products by category only + * Filter products by single category * * @magentoApiDataFixture Magento/Catalog/_files/product_in_multiple_categories.php * @return void @@ -1056,6 +1057,58 @@ public function testFilterProductsBySingleCategoryId() } } + /** + * Sorting the search results by relevance (DESC => most relevant) + * + * @magentoApiDataFixture Magento/Catalog/_files/product_in_multiple_categories.php + * @return void + */ + public function testFilterProductsAndSortByRelevance() + { + $search_term ="red white blue grey socks"; + $query + = <<graphQlQuery($query); + $this->assertEquals(2, $response['products']['total_count']); + } + /** * Sorting by price in the DESC order from the filtered items with default pageSize * @@ -1064,7 +1117,6 @@ public function testFilterProductsBySingleCategoryId() */ public function testQuerySortByPriceDESCWithDefaultPageSize() { - $query = <<get(ProductRepositoryInterface::class); + + $prod1 = $productRepository->get('simple2'); + $prod2 = $productRepository->get('simple1'); + $filteredProducts = [$prod1, $prod2]; + /** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ + $categoryLinkManagement = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); + foreach ($filteredProducts as $product) { + $categoryLinkManagement->assignProductToCategories( + $product->getSku(), + [333] + ); + } + $query =<<graphQlQuery($query); $this->assertEquals(2, $response['products']['total_count']); - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - - $prod1 = $productRepository->get('simple2'); - $prod2 = $productRepository->get('simple1'); - $filteredProducts = [$prod1, $prod2]; $this->assertProductItemsWithPriceCheck($filteredProducts, $response); + //verify that by default Price and category are the only layers available + $filterNames = ['Price', 'Category']; + $this->assertCount(2, $response['products']['filters'], 'Filter count does not match'); + for ($i = 0; $i < count($response['products']['filters']); $i++) { + $this->assertEquals($filterNames[$i], $response['products']['filters'][$i]['name']); + } } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php index 5990211f1e47d..378b87fb9591f 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php @@ -795,6 +795,7 @@ private function assertOptions($product, $actualResponse) ]; $this->assertResponseFields($value, $assertionMapValues); } else { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $assertionMap = array_merge( $assertionMap, [ @@ -823,7 +824,7 @@ private function assertOptions($product, $actualResponse) $valueKeyName = 'date_option'; $valueAssertionMap = []; } - + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $valueAssertionMap = array_merge( $valueAssertionMap, [ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting.php new file mode 100644 index 0000000000000..e25bd21b7683d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting.php @@ -0,0 +1,75 @@ +create(\Magento\Catalog\Model\Category::class); +$category->isObjectNew(true); +$category->setId( + 333 +)->setCreatedAt( + '2019-08-27 11:05:07' +)->setName( + 'Colorful Category' +)->setParentId( + 2 +)->setPath( + '1/2/300' +)->setLevel( + 2 +)->setAvailableSortBy( + ['position', 'name'] +)->setDefaultSortBy( + 'name' +)->setIsActive( + true +)->setPosition( + 1 +)->save(); + +$defaultAttributeSet = $objectManager->get(Magento\Eav\Model\Config::class) + ->getEntityType('catalog_product') + ->getDefaultAttributeSetId(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($defaultAttributeSet) + ->setStoreId(1) + ->setWebsiteIds([1]) + ->setName('Red White and Blue striped Shoes') + ->setSku('red white and blue striped shoes') + ->setPrice(40) + ->setWeight(8) + ->setDescription('Red white and blue flip flops at one') + ->setMetaTitle('Multi colored shoes meta title') + ->setMetaKeyword('red, white,flip-flops, women, kids') + ->setMetaDescription('flip flops women kids meta description') + ->setStockData(['use_config_manage_stock' => 0]) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->save(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$skus = ['green_socks', 'white_shorts','red_trousers','blue_briefs','grey_shorts', 'red white and blue striped shoes' ]; +$products = []; +foreach ($skus as $sku) { + $products = $productRepository->get($sku); +} +/** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ + $categoryLinkManagement = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); +foreach ($products as $product) { + $categoryLinkManagement->assignProductToCategories( + $product->getSku(), + [300] + ); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php index 6cd2819c2d296..864a931359bdd 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php @@ -115,7 +115,8 @@ 'catalog_product', $attributeSet->getId(), $attributeSet->getDefaultGroupId(), - $attribute1->getId()); + $attribute1->getId() + ); } $eavConfig->clear(); From 6971c763d544013902b6f12961ddf0e557718e1f Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 28 Aug 2019 10:07:19 -0500 Subject: [PATCH 040/147] MC-19618: Update schema for layered navigation output - review fixes --- .../Product/LayeredNavigation/Builder/Attribute.php | 2 +- .../Product/LayeredNavigation/Builder/Category.php | 2 +- .../Product/LayeredNavigation/Builder/Price.php | 2 +- .../{Builder => }/Formatter/LayerFormatter.php | 2 +- app/code/Magento/CatalogGraphQl/etc/schema.graphqls | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) rename app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/{Builder => }/Formatter/LayerFormatter.php (97%) diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php index 89df1be8d59f2..b70c9f6165fc6 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php @@ -12,7 +12,7 @@ use Magento\Framework\Api\Search\AggregationInterface; use Magento\Framework\Api\Search\AggregationValueInterface; use Magento\Framework\Api\Search\BucketInterface; -use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Formatter\LayerFormatter; +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Formatter\LayerFormatter; /** * @inheritdoc diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php index 97644328abc2a..cebafe31385ba 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php @@ -15,7 +15,7 @@ use Magento\Framework\Api\Search\AggregationValueInterface; use Magento\Framework\Api\Search\BucketInterface; use Magento\Framework\App\ResourceConnection; -use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Formatter\LayerFormatter; +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Formatter\LayerFormatter; /** * @inheritdoc diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php index e84a8f1b11078..02b638edbdce8 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php @@ -10,7 +10,7 @@ use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\LayerBuilderInterface; use Magento\Framework\Api\Search\AggregationInterface; use Magento\Framework\Api\Search\BucketInterface; -use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Formatter\LayerFormatter; +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Formatter\LayerFormatter; /** * @inheritdoc diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Formatter/LayerFormatter.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php similarity index 97% rename from app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Formatter/LayerFormatter.php rename to app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php index 72495f8ef8524..48a1265b10fc3 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Formatter/LayerFormatter.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Formatter; +namespace Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Formatter; /** * Format Layered Navigation Items diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 7fe5374611a70..20bc5ef9949a6 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -391,10 +391,10 @@ type MediaGalleryEntry @doc(description: "MediaGalleryEntry defines characterist } type LayerFilter { - name: String @doc(description: "Layered navigation filter name.") - request_var: String @doc(description: "Request variable name for filter query.") - filter_items_count: Int @doc(description: "Count of filter items in filter group.") - filter_items: [LayerFilterItemInterface] @doc(description: "Array of filter items.") + name: String @doc(description: "Layered navigation filter name.") @deprecated(reason: "Use Aggregation.label instead") + request_var: String @doc(description: "Request variable name for filter query.") @deprecated(reason: "Use Aggregation.attribute_code instead") + filter_items_count: Int @doc(description: "Count of filter items in filter group.") @deprecated(reason: "Use Aggregation.count instead") + filter_items: [LayerFilterItemInterface] @doc(description: "Array of filter items.") @deprecated(reason: "Use Aggregation.options instead") } interface LayerFilterItemInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\LayerFilterItemTypeResolverComposite") { From 08de5cfce0fb0a27f41d6991b3fdc16a3840b6b2 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 28 Aug 2019 11:29:41 -0500 Subject: [PATCH 041/147] MC-19618: Update schema for layered navigation output - review fixes --- app/code/Magento/CatalogGraphQl/etc/schema.graphqls | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 20bc5ef9949a6..d29a24428ae12 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -404,14 +404,14 @@ interface LayerFilterItemInterface @typeResolver(class: "Magento\\CatalogGraphQl } type Aggregation { - count: Int @doc(description: "Count of filter items in filter group.") - label: String @doc(description: "Layered navigation filter name.") + count: Int @doc(description: "The number of filter items in the filter group.") + label: String @doc(description: "The filter named displayed in layered navigation.") attribute_code: String! @doc(description: "Attribute code of the filter item.") - options: [AggregationOption] @doc(description: "Array of aggregation options.") + options: [AggregationOption] @doc(description: "Describes each aggregated filter option.") } type AggregationOption { - count: Int @doc(description: "Count of items by filter.") + count: Int @doc(description: "The number of items returned by the filter.") label: String! @doc(description: "Filter label.") value: String! @doc(description: "Value for filter request variable to be used in query.") } From 8a2fbffbad2da4c9b00e84cfc40d2d59c846379d Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Wed, 28 Aug 2019 12:14:33 -0500 Subject: [PATCH 042/147] MC-19702: Add input_type to customAttributeMetadata query --- .../Resolver/CustomAttributeMetadata.php | 14 ++++- .../Model/Resolver/Query/FrontendType.php | 61 +++++++++++++++++++ .../Magento/EavGraphQl/etc/schema.graphqls | 1 + 3 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 app/code/Magento/EavGraphQl/Model/Resolver/Query/FrontendType.php diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php b/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php index 62e3f01836619..85445580bb1fb 100644 --- a/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php +++ b/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php @@ -9,6 +9,7 @@ use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\EavGraphQl\Model\Resolver\Query\Type; +use Magento\EavGraphQl\Model\Resolver\Query\FrontendType; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; @@ -26,12 +27,19 @@ class CustomAttributeMetadata implements ResolverInterface */ private $type; + /** + * @var FrontendType + */ + private $frontendType; + /** * @param Type $type + * @param FrontendType $frontendType */ - public function __construct(Type $type) + public function __construct(Type $type, FrontendType $frontendType) { $this->type = $type; + $this->frontendType = $frontendType; } /** @@ -52,6 +60,7 @@ public function resolve( continue; } try { + $frontendType = $this->frontendType->getType($attribute['attribute_code'], $attribute['entity_type']); $type = $this->type->getType($attribute['attribute_code'], $attribute['entity_type']); } catch (InputException $exception) { $attributes['items'][] = new GraphQlNoSuchEntityException( @@ -78,7 +87,8 @@ public function resolve( $attributes['items'][] = [ 'attribute_code' => $attribute['attribute_code'], 'entity_type' => $attribute['entity_type'], - 'attribute_type' => ucfirst($type) + 'attribute_type' => ucfirst($type), + 'input_type' => $frontendType ]; } diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/Query/FrontendType.php b/app/code/Magento/EavGraphQl/Model/Resolver/Query/FrontendType.php new file mode 100644 index 0000000000000..c76f19e6dfeb4 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/Query/FrontendType.php @@ -0,0 +1,61 @@ +attributeRepository = $attributeRepository; + $this->serviceTypeMap = $serviceTypeMap; + } + + /** + * Return frontend type for attribute + * + * @param string $attributeCode + * @param string $entityType + * @return null|string + */ + public function getType(string $attributeCode, string $entityType): ?string + { + $mappedEntityType = $this->serviceTypeMap->getEntityType($entityType); + if ($mappedEntityType) { + $entityType = $mappedEntityType; + } + try { + $attribute = $this->attributeRepository->get($entityType, $attributeCode); + } catch (NoSuchEntityException $e) { + return null; + } + return $attribute->getFrontendInput(); + } +} diff --git a/app/code/Magento/EavGraphQl/etc/schema.graphqls b/app/code/Magento/EavGraphQl/etc/schema.graphqls index 0b174fbc4d84d..21aa7001fab2b 100644 --- a/app/code/Magento/EavGraphQl/etc/schema.graphqls +++ b/app/code/Magento/EavGraphQl/etc/schema.graphqls @@ -13,6 +13,7 @@ type Attribute @doc(description: "Attribute contains the attribute_type of the s attribute_code: String @doc(description: "The unique identifier for an attribute code. This value should be in lowercase letters without spaces.") entity_type: String @doc(description: "The type of entity that defines the attribute") attribute_type: String @doc(description: "The data type of the attribute") + input_type: String @doc(description: "The frontend input type of the attribute") attribute_options: [AttributeOption] @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\AttributeOptions") @doc(description: "Attribute options list.") } From 53ffcd761360d5d2fc3840e3227b744d8e3b318b Mon Sep 17 00:00:00 2001 From: Raoul Rego Date: Wed, 28 Aug 2019 14:38:21 -0500 Subject: [PATCH 043/147] MC-17627: Dependency static test does not analyze content of phtml files - Fix handling of comment sanitation - Fix incorrect urls - Added checking route by frontName --- .../catalog/product/edit/super/config.phtml | 2 +- .../TestFramework/Dependency/PhpRule.php | 31 ++++++++++++++----- .../Dependency/Route/RouteMapper.php | 9 ++++++ .../Magento/Test/Integrity/DependencyTest.php | 26 +++++++--------- .../dependency_test/whitelist/routes_ce.php | 2 +- 5 files changed, 45 insertions(+), 25 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml index c11a1adc19896..240c5e65c79c3 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml @@ -64,7 +64,7 @@ "productsProvider": "configurable_associated_product_listing.data_source", "productsMassAction": "configurable_associated_product_listing.configurable_associated_product_listing.product_columns.ids", "productsColumns": "configurable_associated_product_listing.configurable_associated_product_listing.product_columns", - "productsGridUrl": "getUrl('catalog/product/associated_grid', ['componentJson' => true]) ?>", + "productsGridUrl": "getUrl('catalog/product_associated/grid', ['componentJson' => true]) ?>", "configurableVariations": "configurableVariations" } } diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index 4958795412681..990df68a95cf3 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -288,8 +288,8 @@ private function isPluginDependency($dependent, $dependency) */ protected function _caseGetUrl(string $currentModule, string &$contents): array { - $pattern = '#(\->|:)(?getUrl\(([\'"])(?[a-z0-9\-_]{3,})' - .'(/(?[a-z0-9\-_]+))?(/(?[a-z0-9\-_]+))?\3)#i'; + $pattern = '#(\->|:)(?getUrl\(([\'"])(?[a-z0-9\-_]{3,}|\*)' + .'(/(?[a-z0-9\-_]+|\*))?(/(?[a-z0-9\-_]+))?\3|\*)#i'; $dependencies = []; if (!preg_match_all($pattern, $contents, $matches, PREG_SET_ORDER)) { @@ -298,10 +298,27 @@ protected function _caseGetUrl(string $currentModule, string &$contents): array try { foreach ($matches as $item) { + $routeId = $item['route_id']; + $controllerName = $item['controller_name'] ?? UrlInterface::DEFAULT_CONTROLLER_NAME; + $actionName = $item['action_name'] ?? UrlInterface::DEFAULT_ACTION_NAME; + if ( + in_array( + implode('/', [$routeId, $controllerName, $actionName]), + $this->getRoutesWhitelist())) { + continue; + } + // skip rest + if($routeId == "rest") { //MC-17627 + continue; + } + // skip wildcards + if($routeId == "*" || $controllerName == "*" || $actionName == "*" ) { //MC-17627 + continue; + } $modules = $this->routeMapper->getDependencyByRoutePath( - $item['route_id'], - $item['controller_name'] ?? UrlInterface::DEFAULT_CONTROLLER_NAME, - $item['action_name'] ?? UrlInterface::DEFAULT_ACTION_NAME + $routeId, + $controllerName, + $actionName ); if (!in_array($currentModule, $modules)) { if (count($modules) === 1) { @@ -315,9 +332,7 @@ protected function _caseGetUrl(string $currentModule, string &$contents): array } } } catch (NoSuchActionException $e) { - if (array_search($e->getMessage(), $this->getRoutesWhitelist()) === false) { - throw new LocalizedException(__('Invalid URL path: %1', $e->getMessage()), $e); - } + throw new LocalizedException(__('Invalid URL path: %1', $e->getMessage()), $e); } return $dependencies; diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php index 87cc0985a053b..a3d6fbffa8c7e 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php @@ -243,6 +243,15 @@ private function processConfigFile(string $module, string $configFile) if (!in_array($module, $this->routers[$routerId][$routeId])) { $this->routers[$routerId][$routeId][] = $module; } + if(isset($route['frontName'])) { + $frontName = (string)$route['frontName']; + if (!isset($this->routers[$routerId][$frontName])) { + $this->routers[$routerId][$frontName] = []; + } + if (!in_array($module, $this->routers[$routerId][$frontName])) { + $this->routers[$routerId][$frontName][] = $module; + } + } } } } diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php index 540fcc76349d1..e52c31723725b 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php @@ -284,21 +284,20 @@ private static function getRoutesWhitelist(): array */ protected function _getCleanedFileContents($fileType, $file) { - $contents = (string)file_get_contents($file); switch ($fileType) { case 'php': - //Removing php comments - $contents = preg_replace('~/\*.*?\*/~m', '', $contents); - $contents = preg_replace('~^\s*/\*.*?\*/~sm', '', $contents); - $contents = preg_replace('~^\s*//.*$~m', '', $contents); - break; + return php_strip_whitespace($file); case 'layout': case 'config': //Removing xml comments - $contents = preg_replace('~\~s', '', $contents); + return preg_replace( + '~\~s', + '', + (string)file_get_contents($file) + ); break; case 'template': - //Removing html + $contents = php_strip_whitespace($file); $contentsWithoutHtml = ''; preg_replace_callback( '~(<\?(php|=)\s+.*\?>)~sU', @@ -308,15 +307,13 @@ function ($matches) use ($contents, &$contentsWithoutHtml) { }, $contents ); - $contents = $contentsWithoutHtml; - //Removing php comments - $contents = preg_replace('~/\*.*?\*/~s', '', $contents); - $contents = preg_replace('~^\s*//.*$~s', '', $contents); - break; } - return $contents; + + return (string)file_get_contents($file); } + + /** * @inheritdoc * @throws \Exception @@ -395,7 +392,6 @@ protected function getDependenciesFromFiles($module, $fileType, $file, $contents $newDependencies = $rule->getDependencyInfo($module, $fileType, $file, $contents); $dependencies = array_merge($dependencies, $newDependencies); } - foreach ($dependencies as $key => $dependency) { foreach (self::$whiteList as $namespace) { if (strpos($dependency['source'], $namespace) !== false) { diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/whitelist/routes_ce.php b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/whitelist/routes_ce.php index 9ebc951a3a3a1..1aef3ffdf104a 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/whitelist/routes_ce.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/whitelist/routes_ce.php @@ -6,5 +6,5 @@ declare(strict_types=1); return [ - 'privacy-policy-cookie-restriction-mode/index/index', + 'privacy-policy-cookie-restriction-mode/index/index' ]; From a2a2e16e000eed074d7219a2519cab9a5d740a95 Mon Sep 17 00:00:00 2001 From: Raoul Rego Date: Wed, 28 Aug 2019 16:20:14 -0500 Subject: [PATCH 044/147] MC-17627: Dependency static test does not analyze content of phtml files - Fixed depedencies of modules --- app/code/Magento/Braintree/composer.json | 6 +++--- app/code/Magento/Catalog/composer.json | 3 +-- app/code/Magento/CatalogWidget/composer.json | 3 ++- app/code/Magento/SendFriend/composer.json | 3 ++- app/code/Magento/Translation/composer.json | 3 ++- app/code/Magento/Vault/composer.json | 3 ++- .../testsuite/Magento/Test/Integrity/DependencyTest.php | 2 +- 7 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/code/Magento/Braintree/composer.json b/app/code/Magento/Braintree/composer.json index 5b5eeaf2b3dd7..58049f7bf0f93 100644 --- a/app/code/Magento/Braintree/composer.json +++ b/app/code/Magento/Braintree/composer.json @@ -22,11 +22,11 @@ "magento/module-sales": "*", "magento/module-ui": "*", "magento/module-vault": "*", - "magento/module-multishipping": "*" + "magento/module-multishipping": "*", + "magento/module-theme": "*" }, "suggest": { - "magento/module-checkout-agreements": "*", - "magento/module-theme": "*" + "magento/module-checkout-agreements": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Catalog/composer.json b/app/code/Magento/Catalog/composer.json index fa8daaabe5710..8023634fa074d 100644 --- a/app/code/Magento/Catalog/composer.json +++ b/app/code/Magento/Catalog/composer.json @@ -31,8 +31,7 @@ "magento/module-ui": "*", "magento/module-url-rewrite": "*", "magento/module-widget": "*", - "magento/module-wishlist": "*", - "magento/module-authorization": "*" + "magento/module-wishlist": "*" }, "suggest": { "magento/module-cookie": "*", diff --git a/app/code/Magento/CatalogWidget/composer.json b/app/code/Magento/CatalogWidget/composer.json index 6722d0df93752..8c1bd220a0f32 100644 --- a/app/code/Magento/CatalogWidget/composer.json +++ b/app/code/Magento/CatalogWidget/composer.json @@ -14,7 +14,8 @@ "magento/module-rule": "*", "magento/module-store": "*", "magento/module-widget": "*", - "magento/module-wishlist": "*" + "magento/module-wishlist": "*", + "magento/module-theme": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/SendFriend/composer.json b/app/code/Magento/SendFriend/composer.json index f06f1b4a9e3e3..064b45e97d6c5 100644 --- a/app/code/Magento/SendFriend/composer.json +++ b/app/code/Magento/SendFriend/composer.json @@ -11,7 +11,8 @@ "magento/module-customer": "*", "magento/module-store": "*", "magento/module-captcha": "*", - "magento/module-authorization": "*" + "magento/module-authorization": "*", + "magento/module-theme": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Translation/composer.json b/app/code/Magento/Translation/composer.json index c01791c88f99f..511238aefe7f3 100644 --- a/app/code/Magento/Translation/composer.json +++ b/app/code/Magento/Translation/composer.json @@ -9,7 +9,8 @@ "magento/framework": "*", "magento/module-backend": "*", "magento/module-developer": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-theme" : "*" }, "suggest": { "magento/module-deploy": "*" diff --git a/app/code/Magento/Vault/composer.json b/app/code/Magento/Vault/composer.json index 7dc2e0be78640..c37bc51f9d432 100644 --- a/app/code/Magento/Vault/composer.json +++ b/app/code/Magento/Vault/composer.json @@ -12,7 +12,8 @@ "magento/module-payment": "*", "magento/module-quote": "*", "magento/module-sales": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-theme": "*" }, "type": "magento2-module", "license": [ diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php index e52c31723725b..c890cee6f1dda 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php @@ -307,8 +307,8 @@ function ($matches) use ($contents, &$contentsWithoutHtml) { }, $contents ); + return $contentsWithoutHtml; } - return (string)file_get_contents($file); } From 3bc203941125294fd3fabc9b2bacfd74da468a90 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Wed, 28 Aug 2019 16:52:19 -0500 Subject: [PATCH 045/147] MC-18512: Dynamically inject all searchable custom attributes for product filtering - Use FilterMatchTypeInput --- .../Model/Config/FilterAttributeReader.php | 19 ++++++++++++------- .../Model/Resolver/Products.php | 4 ++-- app/code/Magento/GraphQl/etc/schema.graphqls | 5 ++--- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php index b2cb4dca28bde..3238a1b6564ad 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php +++ b/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php @@ -11,6 +11,7 @@ use Magento\Framework\GraphQl\Schema\Type\Entity\MapperInterface; use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; /** * Adds custom/eav attributes to product filter type in the GraphQL config. @@ -32,7 +33,7 @@ class FilterAttributeReader implements ReaderInterface */ private const FILTER_EQUAL_TYPE = 'FilterEqualTypeInput'; private const FILTER_RANGE_TYPE = 'FilterRangeTypeInput'; - private const FILTER_LIKE_TYPE = 'FilterLikeTypeInput'; + private const FILTER_MATCH_TYPE = 'FilterMatchTypeInput'; /** * @var MapperInterface @@ -74,7 +75,7 @@ public function read($scope = null) : array foreach ($typeNames as $typeName) { $config[$typeName]['fields'][$attributeCode] = [ 'name' => $attributeCode, - 'type' => $this->getFilterType($attribute->getFrontendInput()), + 'type' => $this->getFilterType($attribute), 'arguments' => [], 'required' => false, 'description' => sprintf('Attribute label: %s', $attribute->getDefaultFrontendLabel()) @@ -88,22 +89,26 @@ public function read($scope = null) : array /** * Map attribute type to filter type * - * @param string $attributeType + * @param Attribute $attribute * @return string */ - private function getFilterType($attributeType): string + private function getFilterType(Attribute $attribute): string { + if ($attribute->getAttributeCode() === 'sku') { + return self::FILTER_EQUAL_TYPE; + } + $filterTypeMap = [ 'price' => self::FILTER_RANGE_TYPE, 'date' => self::FILTER_RANGE_TYPE, 'select' => self::FILTER_EQUAL_TYPE, 'multiselect' => self::FILTER_EQUAL_TYPE, 'boolean' => self::FILTER_EQUAL_TYPE, - 'text' => self::FILTER_LIKE_TYPE, - 'textarea' => self::FILTER_LIKE_TYPE, + 'text' => self::FILTER_MATCH_TYPE, + 'textarea' => self::FILTER_MATCH_TYPE, ]; - return $filterTypeMap[$attributeType] ?? self::FILTER_LIKE_TYPE; + return $filterTypeMap[$attribute->getFrontendInput()] ?? self::FILTER_MATCH_TYPE; } /** diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php index 3bc61a0fb3f2c..691f93e4148bc 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php @@ -97,8 +97,8 @@ public function resolve( //get product children fields queried $productFields = (array)$info->getFieldSelection(1); - - $searchCriteria = $this->searchApiCriteriaBuilder->build($args, isset($productFields['filters'])); + $includeAggregations = isset($productFields['filters']) || isset($productFields['aggregations']); + $searchCriteria = $this->searchApiCriteriaBuilder->build($args, $includeAggregations); $searchResult = $this->searchQuery->getResult($searchCriteria, $info, $args); if ($searchResult->getCurrentPage() > $searchResult->getTotalPages() && $searchResult->getTotalCount() > 0) { diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls index 997572471122f..69b822d4285b8 100644 --- a/app/code/Magento/GraphQl/etc/schema.graphqls +++ b/app/code/Magento/GraphQl/etc/schema.graphqls @@ -75,9 +75,8 @@ input FilterRangeTypeInput @doc(description: "Specifies which action will be per to: String } -input FilterLikeTypeInput @doc(description: "Specifies which action will be performed in a query ") { - like: String - eq: String +input FilterMatchTypeInput @doc(description: "Specifies which action will be performed in a query ") { + match: String } type SearchResultPageInfo @doc(description: "SearchResultPageInfo provides navigation for the query response") { From 4760f86ba4c8ad2f24f6db077a7998359574e5eb Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Thu, 29 Aug 2019 15:56:49 -0500 Subject: [PATCH 046/147] MC-18512: Dynamically inject all searchable custom attributes for product filtering --- app/code/Magento/CatalogGraphQl/etc/schema.graphqls | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index d29a24428ae12..8e9471c77dc6b 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -7,7 +7,7 @@ type Query { filter: ProductAttributeFilterInput @doc(description: "Identifies which product attributes to search for and return."), pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), - sort: ProductAttributeSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.") + sort: ProductAttributeSortInput @doc(description: "Specifies which attributes to sort on, and whether to return the results in ascending or descending order.") ): Products @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Products") @doc(description: "The products query searches for products that match the criteria specified in the search and filter attributes.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") category ( @@ -221,7 +221,7 @@ interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model products( pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), - sort: ProductAttributeSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.") + sort: ProductAttributeSortInput @doc(description: "Specifies which attributes to sort on, and whether to return the results in ascending or descending order.") ): CategoryProducts @doc(description: "The list of products assigned to the category.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Products") breadcrumbs: [Breadcrumb] @doc(description: "Breadcrumbs, parent categories info.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Breadcrumbs") } @@ -338,7 +338,7 @@ type ProductMediaGalleryEntriesVideoContent @doc(description: "ProductMediaGalle video_metadata: String @doc(description: "Optional data about the video.") } -input ProductSortInput @deprecated(reason: "Attributes used in this input are hardcoded and some of them are not searcheable. Use @ProductAttributeSortInput instead") @doc(description: "ProductSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order.") { +input ProductSortInput @deprecated(reason: "The attributes used in this input are hard-coded, and some of them are not sortable. Use @ProductAttributeSortInput instead") @doc(description: "ProductSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order.") { name: SortEnum @doc(description: "The product name. Customers use this name to identify the product.") sku: SortEnum @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") description: SortEnum @doc(description: "Detailed information about the product. The value can include simple HTML tags.") From ab3b7d8d4ea35e5728a20a6916d00b25093c937a Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Thu, 29 Aug 2019 16:36:54 -0500 Subject: [PATCH 047/147] MC-19702: Add input_type to customAttributeMetadata query - update test --- .../Catalog/ProductAttributeTypeTest.php | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php index 063da7c11bf7f..a34d5e21704af 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php @@ -50,7 +50,8 @@ public function testAttributeTypeResolver() { attribute_code attribute_type - entity_type + entity_type + input_type } } } @@ -71,7 +72,8 @@ public function testAttributeTypeResolver() \Magento\Catalog\Api\Data\ProductInterface::class ]; $attributeTypes = ['String', 'Int', 'Float','Boolean', 'Float']; - $this->assertAttributeType($attributeTypes, $expectedAttributeCodes, $entityType, $response); + $inputTypes = ['textarea', 'select', 'price', 'boolean', 'price']; + $this->assertAttributeType($attributeTypes, $expectedAttributeCodes, $entityType, $inputTypes, $response); } /** @@ -121,7 +123,8 @@ public function testComplexAttributeTypeResolver() { attribute_code attribute_type - entity_type + entity_type + input_type } } } @@ -154,7 +157,16 @@ public function testComplexAttributeTypeResolver() 'CustomerDataRegionInterface', 'ProductMediaGallery' ]; - $this->assertAttributeType($attributeTypes, $expectedAttributeCodes, $entityTypes, $response); + $inputTypes = [ + 'select', + 'multiselect', + 'select', + 'select', + 'text', + 'text', + 'gallery' + ]; + $this->assertAttributeType($attributeTypes, $expectedAttributeCodes, $entityTypes, $inputTypes, $response); } /** @@ -213,11 +225,17 @@ public function testUnDefinedAttributeType() * @param array $attributeTypes * @param array $expectedAttributeCodes * @param array $entityTypes + * @param array $inputTypes * @param array $actualResponse * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - private function assertAttributeType($attributeTypes, $expectedAttributeCodes, $entityTypes, $actualResponse) - { + private function assertAttributeType( + $attributeTypes, + $expectedAttributeCodes, + $entityTypes, + $inputTypes, + $actualResponse + ) { $attributeMetaDataItems = array_map(null, $actualResponse['customAttributeMetadata']['items'], $attributeTypes); foreach ($attributeMetaDataItems as $itemIndex => $itemArray) { @@ -225,8 +243,9 @@ private function assertAttributeType($attributeTypes, $expectedAttributeCodes, $ $attributeMetaDataItems[$itemIndex][0], [ "attribute_code" => $expectedAttributeCodes[$itemIndex], - "attribute_type" =>$attributeTypes[$itemIndex], - "entity_type" => $entityTypes[$itemIndex] + "attribute_type" => $attributeTypes[$itemIndex], + "entity_type" => $entityTypes[$itemIndex], + "input_type" => $inputTypes[$itemIndex] ] ); } From 2a8291985b4b0d5a79ca8df0fe1c27d4040d934d Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Fri, 30 Aug 2019 11:55:57 -0500 Subject: [PATCH 048/147] MC-18450: Allow custom attributes to be filterable and also returned in the layered navigation the product filter section in GraphQL - Allow exact filtering by sku --- .../Plugin/Search/Request/ConfigReader.php | 207 ++++++++++++------ .../FieldMapper/Product/AttributeAdapter.php | 13 ++ .../Product/FieldProvider/StaticField.php | 12 + 3 files changed, 166 insertions(+), 66 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php index 151be89177744..02f23e59f15d7 100644 --- a/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php +++ b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php @@ -8,9 +8,12 @@ use Magento\Catalog\Api\Data\EavAttributeInterface; use Magento\CatalogSearch\Model\Search\RequestGenerator; use Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorResolver; +use Magento\Eav\Model\Entity\Attribute; +use Magento\Framework\Search\EngineResolverInterface; use Magento\Framework\Search\Request\FilterInterface; use Magento\Framework\Search\Request\QueryInterface; use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection; /** * Add search request configuration to config for give ability filter and search products during GraphQL request @@ -42,19 +45,27 @@ class ConfigReader */ private $productAttributeCollectionFactory; + /** + * @var EngineResolverInterface + */ + private $searchEngineResolver; + /** Bucket name suffix */ private const BUCKET_SUFFIX = '_bucket'; /** * @param GeneratorResolver $generatorResolver * @param CollectionFactory $productAttributeCollectionFactory + * @param EngineResolverInterface $searchEngineResolver */ public function __construct( GeneratorResolver $generatorResolver, - CollectionFactory $productAttributeCollectionFactory + CollectionFactory $productAttributeCollectionFactory, + EngineResolverInterface $searchEngineResolver ) { $this->generatorResolver = $generatorResolver; $this->productAttributeCollectionFactory = $productAttributeCollectionFactory; + $this->searchEngineResolver = $searchEngineResolver; } /** @@ -86,19 +97,19 @@ public function afterRead( /** * Retrieve searchable attributes * - * @return \Magento\Eav\Model\Entity\Attribute[] + * @return Attribute[] */ private function getSearchableAttributes(): array { $attributes = []; - /** @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection $productAttributes */ + /** @var Collection $productAttributes */ $productAttributes = $this->productAttributeCollectionFactory->create(); $productAttributes->addFieldToFilter( ['is_searchable', 'is_visible_in_advanced_search', 'is_filterable', 'is_filterable_in_search'], [1, 1, [1, 2], 1] ); - /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ + /** @var Attribute $attribute */ foreach ($productAttributes->getItems() as $attribute) { $attributes[$attribute->getAttributeCode()] = $attribute; } @@ -121,83 +132,35 @@ private function generateRequest() continue; } $queryName = $attribute->getAttributeCode() . '_query'; + $filterName = $attribute->getAttributeCode() . RequestGenerator::FILTER_SUFFIX; $request['queries'][$this->requestNameWithAggregation]['queryReference'][] = [ 'clause' => 'must', 'ref' => $queryName, ]; + switch ($attribute->getBackendType()) { case 'static': case 'text': case 'varchar': if ($attribute->getFrontendInput() === 'multiselect') { - $filterName = $attribute->getAttributeCode() . RequestGenerator::FILTER_SUFFIX; - $request['queries'][$queryName] = [ - 'name' => $queryName, - 'type' => QueryInterface::TYPE_FILTER, - 'filterReference' => [ - [ - 'ref' => $filterName, - ], - ], - ]; - $request['filters'][$filterName] = [ - 'type' => FilterInterface::TYPE_TERM, - 'name' => $filterName, - 'field' => $attribute->getAttributeCode(), - 'value' => '$' . $attribute->getAttributeCode() . '$', - ]; + $request['queries'][$queryName] = $this->generateFilterQuery($queryName, $filterName); + $request['filters'][$filterName] = $this->generateTermFilter($filterName, $attribute); + } elseif ($attribute->getAttributeCode() === 'sku') { + $request['queries'][$queryName] = $this->generateFilterQuery($queryName, $filterName); + $request['filters'][$filterName] = $this->generateSkuTermFilter($filterName, $attribute); } else { - $request['queries'][$queryName] = [ - 'name' => $queryName, - 'type' => 'matchQuery', - 'value' => '$' . $attribute->getAttributeCode() . '$', - 'match' => [ - [ - 'field' => $attribute->getAttributeCode(), - 'boost' => $attribute->getSearchWeight() ?: 1, - ], - ], - ]; + $request['queries'][$queryName] = $this->generateMatchQuery($queryName, $attribute); } break; case 'decimal': case 'datetime': case 'date': - $filterName = $attribute->getAttributeCode() . RequestGenerator::FILTER_SUFFIX; - $request['queries'][$queryName] = [ - 'name' => $queryName, - 'type' => QueryInterface::TYPE_FILTER, - 'filterReference' => [ - [ - 'ref' => $filterName, - ], - ], - ]; - $request['filters'][$filterName] = [ - 'field' => $attribute->getAttributeCode(), - 'name' => $filterName, - 'type' => FilterInterface::TYPE_RANGE, - 'from' => '$' . $attribute->getAttributeCode() . '.from$', - 'to' => '$' . $attribute->getAttributeCode() . '.to$', - ]; + $request['queries'][$queryName] = $this->generateFilterQuery($queryName, $filterName); + $request['filters'][$filterName] = $this->generateRangeFilter($filterName, $attribute); break; default: - $filterName = $attribute->getAttributeCode() . RequestGenerator::FILTER_SUFFIX; - $request['queries'][$queryName] = [ - 'name' => $queryName, - 'type' => QueryInterface::TYPE_FILTER, - 'filterReference' => [ - [ - 'ref' => $filterName, - ], - ], - ]; - $request['filters'][$filterName] = [ - 'type' => FilterInterface::TYPE_TERM, - 'name' => $filterName, - 'field' => $attribute->getAttributeCode(), - 'value' => '$' . $attribute->getAttributeCode() . '$', - ]; + $request['queries'][$queryName] = $this->generateFilterQuery($queryName, $filterName); + $request['filters'][$filterName] = $this->generateTermFilter($filterName, $attribute); } $generator = $this->generatorResolver->getGeneratorForType($attribute->getBackendType()); @@ -215,11 +178,11 @@ private function generateRequest() /** * Add attribute with specified boost to "search" query used in full text search * - * @param \Magento\Eav\Model\Entity\Attribute $attribute + * @param Attribute $attribute * @param array $request * @return void */ - private function addSearchAttributeToFullTextSearch(\Magento\Eav\Model\Entity\Attribute $attribute, &$request): void + private function addSearchAttributeToFullTextSearch(Attribute $attribute, &$request): void { // Match search by custom price attribute isn't supported if ($attribute->getFrontendInput() !== 'price') { @@ -229,4 +192,116 @@ private function addSearchAttributeToFullTextSearch(\Magento\Eav\Model\Entity\At ]; } } + + /** + * Return array representation of range filter + * + * @param string $filterName + * @param Attribute $attribute + * @return array + */ + private function generateRangeFilter(string $filterName, Attribute $attribute) + { + return [ + 'field' => $attribute->getAttributeCode(), + 'name' => $filterName, + 'type' => FilterInterface::TYPE_RANGE, + 'from' => '$' . $attribute->getAttributeCode() . '.from$', + 'to' => '$' . $attribute->getAttributeCode() . '.to$', + ]; + } + + /** + * Return array representation of term filter + * + * @param string $filterName + * @param Attribute $attribute + * @return array + */ + private function generateTermFilter(string $filterName, Attribute $attribute) + { + return [ + 'type' => FilterInterface::TYPE_TERM, + 'name' => $filterName, + 'field' => $attribute->getAttributeCode(), + 'value' => '$' . $attribute->getAttributeCode() . '$', + ]; + } + + /** + * Generate term filter for sku field + * + * Sku needs to be treated specially to allow for exact match + * + * @param string $filterName + * @param Attribute $attribute + * @return array + */ + private function generateSkuTermFilter(string $filterName, Attribute $attribute) + { + $field = $this->isElasticSearch() ? 'sku.filter_sku' : 'sku'; + + return [ + 'type' => FilterInterface::TYPE_TERM, + 'name' => $filterName, + 'field' => $field, + 'value' => '$' . $attribute->getAttributeCode() . '$', + ]; + } + + /** + * Return array representation of query based on filter + * + * @param string $queryName + * @param string $filterName + * @return array + */ + private function generateFilterQuery(string $queryName, string $filterName) + { + return [ + 'name' => $queryName, + 'type' => QueryInterface::TYPE_FILTER, + 'filterReference' => [ + [ + 'ref' => $filterName, + ], + ], + ]; + } + + /** + * Return array representation of match query + * + * @param string $queryName + * @param Attribute $attribute + * @return array + */ + private function generateMatchQuery(string $queryName, Attribute $attribute) + { + return [ + 'name' => $queryName, + 'type' => 'matchQuery', + 'value' => '$' . $attribute->getAttributeCode() . '$', + 'match' => [ + [ + 'field' => $attribute->getAttributeCode(), + 'boost' => $attribute->getSearchWeight() ?: 1, + ], + ], + ]; + } + + /** + * Check if the current search engine is elasticsearch + * + * @return bool + */ + private function isElasticSearch() + { + $searchEngine = $this->searchEngineResolver->getCurrentSearchEngine(); + if (strpos($searchEngine, 'elasticsearch') === 0) { + return true; + } + return false; + } } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php index 165f7e78eb65f..c3952b494f2e9 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php @@ -176,6 +176,19 @@ public function getFrontendInput() return $this->getAttribute()->getFrontendInput(); } + /** + * Check if product should always be filterable + * + * @return bool + */ + public function isAlwaysFilterable(): bool + { + // List of attributes which are required to be filterable + $alwaysFilterableAttributes = ['sku']; + + return in_array($this->getAttributeCode(), $alwaysFilterableAttributes, true); + } + /** * Get product attribute instance. * diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php index 6876b23bbb156..466bf5d2d09eb 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php @@ -130,6 +130,18 @@ public function getFields(array $context = []): array ]; } + if ($attributeAdapter->isAlwaysFilterable()) { + $filterFieldName = 'filter_' . $this->fieldNameResolver->getFieldName( + $attributeAdapter, + ['type' => FieldMapperInterface::TYPE_FILTER] + ); + $allAttributes[$fieldName]['fields'][$filterFieldName] = [ + 'type' => $this->fieldTypeConverter->convert( + FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD + ) + ]; + } + if ($attributeAdapter->isComplexType()) { $childFieldName = $this->fieldNameResolver->getFieldName( $attributeAdapter, From ef68feda5d4b289dfb7e3355e23323c5b3101438 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Fri, 30 Aug 2019 15:59:58 -0500 Subject: [PATCH 049/147] MC-18450: Allow custom attributes to be filterable and also returned in the layered navigation the product filter section in GraphQL - Always apply relevance sort order --- .../Product/SearchCriteriaBuilder.php | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php index 1e646b3c0b74b..af6ed85196cf8 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -96,11 +96,9 @@ public function build(array $args, bool $includeAggregation): SearchCriteriaInte if (!empty($args['search'])) { $this->addFilter($searchCriteria, 'search_term', $args['search']); - if (!$searchCriteria->getSortOrders()) { - $this->addDefaultSortOrder($searchCriteria); - } } + $this->addDefaultSortOrder($searchCriteria); $this->addVisibilityFilter($searchCriteria, !empty($args['search']), !empty($args['filter'])); $searchCriteria->setCurrentPage($args['currentPage']); @@ -166,17 +164,27 @@ private function addFilter(SearchCriteriaInterface $searchCriteria, string $fiel } /** - * Sort by _score DESC if no sort order is set + * Sort by relevance DESC by default * * @param SearchCriteriaInterface $searchCriteria */ private function addDefaultSortOrder(SearchCriteriaInterface $searchCriteria): void { - $sortOrder = $this->sortOrderBuilder - ->setField('_score') + $sortOrders = $searchCriteria->getSortOrders() ?? []; + foreach ($sortOrders as $sortOrder) { + // Relevance order is already specified + if ($sortOrder->getField() === 'relevance') { + return; + } + } + $defaultSortOrder = $this->sortOrderBuilder + ->setField('relevance') ->setDirection(SortOrder::SORT_DESC) ->create(); - $searchCriteria->setSortOrders([$sortOrder]); + + $sortOrders[] = $defaultSortOrder; + + $searchCriteria->setSortOrders($sortOrders); } /** From 7d19fe02eaae1fdc824f26a068dc537211eed9d7 Mon Sep 17 00:00:00 2001 From: Buba Suma Date: Fri, 30 Aug 2019 15:24:05 -0500 Subject: [PATCH 050/147] MC-19713: Relative Category links created using PageBuilder have the store name as a URL parameter - Remove store query param from category and product URL on cms pages --- .../Magento/Catalog/Block/Widget/Link.php | 19 +- .../Test/Unit/Block/Widget/LinkTest.php | 221 ++++++++++++------ 2 files changed, 158 insertions(+), 82 deletions(-) diff --git a/app/code/Magento/Catalog/Block/Widget/Link.php b/app/code/Magento/Catalog/Block/Widget/Link.php index 85e50dbd3dc27..3d8a1cdf91ca2 100644 --- a/app/code/Magento/Catalog/Block/Widget/Link.php +++ b/app/code/Magento/Catalog/Block/Widget/Link.php @@ -4,17 +4,15 @@ * See COPYING.txt for license details. */ -/** - * Widget to display catalog link - * - * @author Magento Core Team - */ namespace Magento\Catalog\Block\Widget; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +/** + * Render the URL of given entity + */ class Link extends \Magento\Framework\View\Element\Html\Link implements \Magento\Widget\Block\BlockInterface { /** @@ -63,10 +61,9 @@ public function __construct( /** * Prepare url using passed id path and return it - * or return false if path was not found in url rewrites. * * @throws \RuntimeException - * @return string|false + * @return string|false if path was not found in url rewrites. * @SuppressWarnings(PHPMD.NPathComplexity) */ public function getHref() @@ -92,10 +89,6 @@ public function getHref() if ($rewrite) { $href = $store->getUrl('', ['_direct' => $rewrite->getRequestPath()]); - - if (strpos($href, '___store') === false) { - $href .= (strpos($href, '?') === false ? '?' : '&') . '___store=' . $store->getCode(); - } } $this->_href = $href; } @@ -121,6 +114,7 @@ protected function parseIdPath($idPath) /** * Prepare label using passed text as parameter. + * * If anchor text was not specified get entity name from DB. * * @return string @@ -150,9 +144,8 @@ public function getLabel() /** * Render block HTML - * or return empty string if url can't be prepared * - * @return string + * @return string empty string if url can't be prepared */ protected function _toHtml() { diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php index 8333ed22e1da0..3ceaf7dd44f57 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php @@ -5,54 +5,81 @@ */ namespace Magento\Catalog\Test\Unit\Block\Widget; +use Exception; +use Magento\Catalog\Block\Widget\Link; +use Magento\Catalog\Model\ResourceModel\AbstractResource; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\Framework\App\Config\ReinitableConfigInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Url; +use Magento\Framework\Url\ModifierInterface; +use Magento\Framework\View\Element\Template\Context; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; +use ReflectionClass; +use RuntimeException; -class LinkTest extends \PHPUnit\Framework\TestCase +/** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class LinkTest extends TestCase { /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Store\Model\StoreManagerInterface + * @var PHPUnit_Framework_MockObject_MockObject|StoreManagerInterface */ protected $storeManager; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\UrlRewrite\Model\UrlFinderInterface + * @var PHPUnit_Framework_MockObject_MockObject|UrlFinderInterface */ protected $urlFinder; /** - * @var \Magento\Catalog\Block\Widget\Link + * @var Link */ protected $block; /** - * @var \Magento\Catalog\Model\ResourceModel\AbstractResource|\PHPUnit_Framework_MockObject_MockObject + * @var AbstractResource|PHPUnit_Framework_MockObject_MockObject */ protected $entityResource; + /** + * @inheritDoc + */ protected function setUp() { - $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); - $this->urlFinder = $this->createMock(\Magento\UrlRewrite\Model\UrlFinderInterface::class); + $this->storeManager = $this->createMock(StoreManagerInterface::class); + $this->urlFinder = $this->createMock(UrlFinderInterface::class); - $context = $this->createMock(\Magento\Framework\View\Element\Template\Context::class); + $context = $this->createMock(Context::class); $context->expects($this->any()) ->method('getStoreManager') ->will($this->returnValue($this->storeManager)); $this->entityResource = - $this->createMock(\Magento\Catalog\Model\ResourceModel\AbstractResource::class); - - $this->block = (new ObjectManager($this))->getObject(\Magento\Catalog\Block\Widget\Link::class, [ - 'context' => $context, - 'urlFinder' => $this->urlFinder, - 'entityResource' => $this->entityResource - ]); + $this->createMock(AbstractResource::class); + + $this->block = (new ObjectManager($this))->getObject( + Link::class, + [ + 'context' => $context, + 'urlFinder' => $this->urlFinder, + 'entityResource' => $this->entityResource + ] + ); } /** - * @expectedException \RuntimeException + * Tests getHref with wrong id_path + * + * @expectedException RuntimeException * @expectedExceptionMessage Parameter id_path is not set. */ public function testGetHrefWithoutSetIdPath() @@ -61,7 +88,9 @@ public function testGetHrefWithoutSetIdPath() } /** - * @expectedException \RuntimeException + * Tests getHref with wrong id_path + * + * @expectedException RuntimeException * @expectedExceptionMessage Wrong id_path structure. */ public function testGetHrefIfSetWrongIdPath() @@ -70,27 +99,30 @@ public function testGetHrefIfSetWrongIdPath() $this->block->getHref(); } + /** + * Tests getHref with wrong store ID + * + * @expectedException Exception + */ public function testGetHrefWithSetStoreId() { $this->block->setData('id_path', 'type/id'); $this->block->setData('store_id', 'store_id'); - $this->storeManager->expects($this->once()) - ->method('getStore')->with('store_id') - // interrupt test execution - ->will($this->throwException(new \Exception())); - - try { - $this->block->getHref(); - } catch (\Exception $e) { - } + ->method('getStore') + ->with('store_id') + ->will($this->throwException(new Exception())); + $this->block->getHref(); } + /** + * Tests getHref with not found URL + */ public function testGetHrefIfRewriteIsNotFound() { $this->block->setData('id_path', 'entity_type/entity_id'); - $store = $this->createPartialMock(\Magento\Store\Model\Store::class, ['getId', '__wakeUp']); + $store = $this->createPartialMock(Store::class, ['getId', '__wakeUp']); $store->expects($this->any()) ->method('getId'); @@ -105,52 +137,94 @@ public function testGetHrefIfRewriteIsNotFound() } /** - * @param string $url - * @param string $separator + * Tests getHref whether it should include the store code or not + * * @dataProvider dataProviderForTestGetHrefWithoutUrlStoreSuffix + * @param string $path + * @param bool $includeStoreCode + * @param string $expected + * @throws \ReflectionException */ - public function testGetHrefWithoutUrlStoreSuffix($url, $separator) - { - $storeId = 15; - $storeCode = 'store-code'; - $requestPath = 'request-path'; + public function testStoreCodeShouldBeIncludedInURLOnlyIfItIsConfiguredSo( + string $path, + bool $includeStoreCode, + string $expected + ) { $this->block->setData('id_path', 'entity_type/entity_id'); - - $rewrite = $this->createMock(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class); - $rewrite->expects($this->once()) - ->method('getRequestPath') - ->will($this->returnValue($requestPath)); - - $store = $this->createPartialMock( - \Magento\Store\Model\Store::class, - ['getId', 'getUrl', 'getCode', '__wakeUp'] + $objectManager = new ObjectManager($this); + + $rewrite = $this->createPartialMock(UrlRewrite::class, ['getRequestPath']); + $url = $this->createPartialMock(Url::class, ['setScope', 'getUrl']); + $urlModifier = $this->getMockForAbstractClass(ModifierInterface::class); + $config = $this->getMockForAbstractClass(ReinitableConfigInterface::class); + $store = $objectManager->getObject( + Store::class, + [ + 'storeManager' => $this->storeManager, + 'url' => $url, + 'config' => $config + ] ); - $store->expects($this->once()) - ->method('getId') - ->will($this->returnValue($storeId)); - $store->expects($this->once()) + $property = (new ReflectionClass(get_class($store)))->getProperty('urlModifier'); + $property->setAccessible(true); + $property->setValue($store, $urlModifier); + + $urlModifier->expects($this->any()) + ->method('execute') + ->willReturnArgument(0); + $config->expects($this->any()) + ->method('getValue') + ->willReturnMap( + [ + [Store::XML_PATH_USE_REWRITES, ReinitableConfigInterface::SCOPE_TYPE_DEFAULT, null, true], + [ + Store::XML_PATH_STORE_IN_URL, + ReinitableConfigInterface::SCOPE_TYPE_DEFAULT, + null, $includeStoreCode + ] + ] + ); + + $url->expects($this->any()) + ->method('setScope') + ->willReturnSelf(); + + $url->expects($this->any()) ->method('getUrl') - ->with('', ['_direct' => $requestPath]) - ->will($this->returnValue($url)); - $store->expects($this->once()) - ->method('getCode') - ->will($this->returnValue($storeCode)); + ->willReturnCallback( + function ($route, $params) use ($store) { + return rtrim($store->getBaseUrl(), '/') .'/'. ltrim($params['_direct'], '/'); + } + ); - $this->storeManager->expects($this->once()) + $store->addData(['store_id' => 1, 'code' => 'french']); + + $this->storeManager + ->expects($this->any()) ->method('getStore') - ->will($this->returnValue($store)); + ->willReturn($store); - $this->urlFinder->expects($this->once())->method('findOneByData') - ->with([ + $this->urlFinder->expects($this->once()) + ->method('findOneByData') + ->with( + [ UrlRewrite::ENTITY_ID => 'entity_id', UrlRewrite::ENTITY_TYPE => 'entity_type', - UrlRewrite::STORE_ID => $storeId, - ]) + UrlRewrite::STORE_ID => $store->getStoreId(), + ] + ) ->will($this->returnValue($rewrite)); - $this->assertEquals($url . $separator . '___store=' . $storeCode, $this->block->getHref()); + $rewrite->expects($this->once()) + ->method('getRequestPath') + ->will($this->returnValue($path)); + + $this->assertContains($expected, $this->block->getHref()); } + /** + * Tests getLabel with custom text + */ public function testGetLabelWithCustomText() { $customText = 'Some text'; @@ -158,6 +232,9 @@ public function testGetLabelWithCustomText() $this->assertEquals($customText, $this->block->getLabel()); } + /** + * Tests getLabel without custom text + */ public function testGetLabelWithoutCustomText() { $category = 'Some text'; @@ -178,17 +255,20 @@ public function testGetLabelWithoutCustomText() public function dataProviderForTestGetHrefWithoutUrlStoreSuffix() { return [ - ['url', '?'], - ['url?some_parameter', '&'], + ['/accessories.html', true, 'french/accessories.html'], + ['/accessories.html', false, '/accessories.html'], ]; } + /** + * Tests getHref with product entity and additional category id in the id_path + */ public function testGetHrefWithForProductWithCategoryIdParameter() { $storeId = 15; $this->block->setData('id_path', ProductUrlRewriteGenerator::ENTITY_TYPE . '/entity_id/category_id'); - $store = $this->createPartialMock(\Magento\Store\Model\Store::class, ['getId', '__wakeUp']); + $store = $this->createPartialMock(Store::class, ['getId', '__wakeUp']); $store->expects($this->any()) ->method('getId') ->will($this->returnValue($storeId)); @@ -197,13 +277,16 @@ public function testGetHrefWithForProductWithCategoryIdParameter() ->method('getStore') ->will($this->returnValue($store)); - $this->urlFinder->expects($this->once())->method('findOneByData') - ->with([ - UrlRewrite::ENTITY_ID => 'entity_id', - UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, - UrlRewrite::STORE_ID => $storeId, - UrlRewrite::METADATA => ['category_id' => 'category_id'], - ]) + $this->urlFinder->expects($this->once()) + ->method('findOneByData') + ->with( + [ + UrlRewrite::ENTITY_ID => 'entity_id', + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::STORE_ID => $storeId, + UrlRewrite::METADATA => ['category_id' => 'category_id'], + ] + ) ->will($this->returnValue(false)); $this->block->getHref(); From a8fbb7cad64ee1817b50576adcf6fc41b9ad52c9 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi Date: Mon, 2 Sep 2019 14:39:35 -0500 Subject: [PATCH 051/147] MC-19650: PayPal is not working in B2B Quote --- .../Controller/Express/AbstractExpress.php | 21 ++++++++++++++++--- .../Controller/Express/OnAuthorization.php | 1 + 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php b/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php index 7ad8fe658ec16..895cdb8d4c600 100644 --- a/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php +++ b/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php @@ -9,6 +9,8 @@ use Magento\Framework\App\Action\Action as AppAction; use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\CartInterface; /** @@ -98,6 +100,11 @@ abstract class AbstractExpress extends AppAction implements */ protected $_customerUrl; + /** + * @var CartRepositoryInterface + */ + private $quoteRepository; + /** * @param \Magento\Framework\App\Action\Context $context * @param \Magento\Customer\Model\Session $customerSession @@ -107,6 +114,7 @@ abstract class AbstractExpress extends AppAction implements * @param \Magento\Framework\Session\Generic $paypalSession * @param \Magento\Framework\Url\Helper\Data $urlHelper * @param \Magento\Customer\Model\Url $customerUrl + * @param CartRepositoryInterface $quoteRepository */ public function __construct( \Magento\Framework\App\Action\Context $context, @@ -116,7 +124,8 @@ public function __construct( \Magento\Paypal\Model\Express\Checkout\Factory $checkoutFactory, \Magento\Framework\Session\Generic $paypalSession, \Magento\Framework\Url\Helper\Data $urlHelper, - \Magento\Customer\Model\Url $customerUrl + \Magento\Customer\Model\Url $customerUrl, + CartRepositoryInterface $quoteRepository = null ) { $this->_customerSession = $customerSession; $this->_checkoutSession = $checkoutSession; @@ -128,6 +137,7 @@ public function __construct( parent::__construct($context); $parameters = ['params' => [$this->_configMethod]]; $this->_config = $this->_objectManager->create($this->_configType, $parameters); + $this->quoteRepository = $quoteRepository ?: ObjectManager::getInstance()->get(CartRepositoryInterface::class); } /** @@ -233,7 +243,12 @@ protected function _getCheckoutSession() protected function _getQuote() { if (!$this->_quote) { - $this->_quote = $this->_getCheckoutSession()->getQuote(); + if ($this->_getSession()->getQuoteId()) { + $this->_quote = $this->quoteRepository->get($this->_getSession()->getQuoteId()); + $this->_getCheckoutSession()->replaceQuote($this->_quote); + } else { + $this->_quote = $this->_getCheckoutSession()->getQuote(); + } } return $this->_quote; } @@ -243,7 +258,7 @@ protected function _getQuote() */ public function getCustomerBeforeAuthUrl() { - return; + return null; } /** diff --git a/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php b/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php index 62f4c4c4c457a..0d7ec3fc6f32d 100644 --- a/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php +++ b/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php @@ -155,6 +155,7 @@ public function execute(): ResultInterface } else { $responseContent['redirectUrl'] = $this->urlBuilder->getUrl('paypal/express/review'); $this->_checkoutSession->setQuoteId($quote->getId()); + $this->_getSession()->setQuoteId($quote->getId()); } } catch (ApiProcessableException $e) { $responseContent['success'] = false; From 48935fff1e2bff2540fba0a33f3c640f9a5554e5 Mon Sep 17 00:00:00 2001 From: Raoul Rego Date: Wed, 28 Aug 2019 16:35:55 -0500 Subject: [PATCH 052/147] MC-17627: Dependency static test does not analyze content of phtml files - Fixed codestyle --- app/code/Magento/Translation/composer.json | 2 +- .../TestFramework/Dependency/PhpRule.php | 13 ++--- .../Dependency/Route/RouteMapper.php | 4 +- .../Magento/Test/Integrity/DependencyTest.php | 54 ++++++------------- 4 files changed, 26 insertions(+), 47 deletions(-) diff --git a/app/code/Magento/Translation/composer.json b/app/code/Magento/Translation/composer.json index 511238aefe7f3..e88f44e7cd039 100644 --- a/app/code/Magento/Translation/composer.json +++ b/app/code/Magento/Translation/composer.json @@ -10,7 +10,7 @@ "magento/module-backend": "*", "magento/module-developer": "*", "magento/module-store": "*", - "magento/module-theme" : "*" + "magento/module-theme": "*" }, "suggest": { "magento/module-deploy": "*" diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index 990df68a95cf3..903b1da0ba9ad 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -285,6 +285,7 @@ private function isPluginDependency($dependent, $dependency) * @return array * @throws LocalizedException * @throws \Exception + * @SuppressWarnings(PMD.CyclomaticComplexity) */ protected function _caseGetUrl(string $currentModule, string &$contents): array { @@ -301,18 +302,18 @@ protected function _caseGetUrl(string $currentModule, string &$contents): array $routeId = $item['route_id']; $controllerName = $item['controller_name'] ?? UrlInterface::DEFAULT_CONTROLLER_NAME; $actionName = $item['action_name'] ?? UrlInterface::DEFAULT_ACTION_NAME; - if ( - in_array( - implode('/', [$routeId, $controllerName, $actionName]), - $this->getRoutesWhitelist())) { + if (in_array( + implode('/', [$routeId, $controllerName, $actionName]), + $this->getRoutesWhitelist() + )) { continue; } // skip rest - if($routeId == "rest") { //MC-17627 + if ($routeId == "rest") { //MC-17627 continue; } // skip wildcards - if($routeId == "*" || $controllerName == "*" || $actionName == "*" ) { //MC-17627 + if ($routeId == "*" || $controllerName == "*" || $actionName == "*") { //MC-17627 continue; } $modules = $this->routeMapper->getDependencyByRoutePath( diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php index a3d6fbffa8c7e..8bd6b1479768f 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php @@ -174,7 +174,7 @@ public function getDependencyByRoutePath( $dependencies = []; foreach ($this->getRouterTypes() as $routerId) { if (isset($this->getActionsMap()[$routerId][$routeId][$controllerName][$actionName])) { - $dependencies = array_merge( + $dependencies = array_merge( //phpcs:ignore $dependencies, $this->getActionsMap()[$routerId][$routeId][$controllerName][$actionName] ); @@ -243,7 +243,7 @@ private function processConfigFile(string $module, string $configFile) if (!in_array($module, $this->routers[$routerId][$routeId])) { $this->routers[$routerId][$routeId][] = $module; } - if(isset($route['frontName'])) { + if (isset($route['frontName'])) { $frontName = (string)$route['frontName']; if (!isset($this->routers[$routerId][$frontName])) { $this->routers[$routerId][$frontName] = []; diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php index c890cee6f1dda..d3160be7c72c0 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php @@ -234,8 +234,7 @@ protected static function _initRules() . '/_files/dependency_test/tables_*.php'; $dbRuleTables = []; foreach (glob($replaceFilePattern) as $fileName) { - //phpcs:ignore Generic.PHP.NoSilencedErrors - $dbRuleTables = array_merge($dbRuleTables, @include $fileName); + $dbRuleTables = array_merge($dbRuleTables, @include $fileName); //phpcs:ignore } self::$_rulesInstances = [ new PhpRule( @@ -265,13 +264,15 @@ private static function getRoutesWhitelist(): array { if (is_null(self::$routesWhitelist)) { $routesWhitelistFilePattern = realpath(__DIR__) . '/_files/dependency_test/whitelist/routes_*.php'; - $routesWhitelist = []; - foreach (glob($routesWhitelistFilePattern) as $fileName) { - $routesWhitelist = array_merge($routesWhitelist, include $fileName); - } - self::$routesWhitelist = $routesWhitelist; + self::$routesWhitelist = array_merge( + ...array_map( + function ($fileName) { + return include $fileName; + }, + glob($routesWhitelistFilePattern) + ) + ); } - return self::$routesWhitelist; } @@ -295,9 +296,9 @@ protected function _getCleanedFileContents($fileType, $file) '', (string)file_get_contents($file) ); - break; case 'template': $contents = php_strip_whitespace($file); + //Removing html $contentsWithoutHtml = ''; preg_replace_callback( '~(<\?(php|=)\s+.*\?>)~sU', @@ -312,8 +313,6 @@ function ($matches) use ($contents, &$contentsWithoutHtml) { return (string)file_get_contents($file); } - - /** * @inheritdoc * @throws \Exception @@ -390,7 +389,7 @@ protected function getDependenciesFromFiles($module, $fileType, $file, $contents foreach (self::$_rulesInstances as $rule) { /** @var \Magento\TestFramework\Dependency\RuleInterface $rule */ $newDependencies = $rule->getDependencyInfo($module, $fileType, $file, $contents); - $dependencies = array_merge($dependencies, $newDependencies); + $dependencies = array_merge($dependencies, $newDependencies); //phpcs:ignore } foreach ($dependencies as $key => $dependency) { foreach (self::$whiteList as $namespace) { @@ -505,12 +504,12 @@ public function collectRedundant() foreach (array_keys(self::$mapDependencies) as $module) { $declared = $this->_getDependencies($module, self::TYPE_HARD, self::MAP_TYPE_DECLARED); + //phpcs:ignore $found = array_merge( $this->_getDependencies($module, self::TYPE_HARD, self::MAP_TYPE_FOUND), $this->_getDependencies($module, self::TYPE_SOFT, self::MAP_TYPE_FOUND), $schemaDependencyProvider->getDeclaredExistingModuleDependencies($module) ); - $found['Magento\Framework'] = 'Magento\Framework'; $this->_setDependencies($module, self::TYPE_HARD, self::MAP_TYPE_REDUNDANT, array_diff($declared, $found)); } @@ -574,37 +573,16 @@ protected function _prepareFiles($fileType, $files, $skip = null) */ public function getAllFiles() { - $files = []; - - // Get all php files - $files = array_merge( - $files, + return array_merge( $this->_prepareFiles( 'php', Files::init()->getPhpFiles(Files::INCLUDE_APP_CODE | Files::AS_DATA_SET | Files::INCLUDE_NON_CLASSES), true - ) - ); - - // Get all configuration files - $files = array_merge( - $files, - $this->_prepareFiles('config', Files::init()->getConfigFiles()) - ); - - //Get all layout updates files - $files = array_merge( - $files, - $this->_prepareFiles('layout', Files::init()->getLayoutFiles()) - ); - - // Get all template files - $files = array_merge( - $files, + ), + $this->_prepareFiles('config', Files::init()->getConfigFiles()), + $this->_prepareFiles('layout', Files::init()->getLayoutFiles()), $this->_prepareFiles('template', Files::init()->getPhtmlFiles()) ); - - return $files; } /** From bfa94929a9ac32d35297307498746baa679d002d Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Tue, 3 Sep 2019 11:17:24 -0500 Subject: [PATCH 053/147] MC-19702: Add input_type to customAttributeMetadata query - Fix attribute_options ouput --- .../Model/Resolver/AttributeOptions.php | 24 ++--- .../DataProvider/AttributeOptions.php | 6 +- .../Catalog/ProductAttributeOptionsTest.php | 93 +++++++++++++++++++ 3 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeOptionsTest.php diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php b/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php index e4c27adc60247..7361d52372cd6 100644 --- a/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php +++ b/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php @@ -57,29 +57,31 @@ public function resolve( array $args = null ) : Value { - return $this->valueFactory->create(function () use ($value) { - $entityType = $this->getEntityType($value); - $attributeCode = $this->getAttributeCode($value); + return $this->valueFactory->create( + function () use ($value) { + $entityType = $this->getEntityType($value); + $attributeCode = $this->getAttributeCode($value); - $optionsData = $this->getAttributeOptionsData($entityType, $attributeCode); - return $optionsData; - }); + $optionsData = $this->getAttributeOptionsData($entityType, $attributeCode); + return $optionsData; + } + ); } /** * Get entity type * * @param array $value - * @return int + * @return string * @throws LocalizedException */ - private function getEntityType(array $value): int + private function getEntityType(array $value): string { if (!isset($value['entity_type'])) { throw new LocalizedException(__('"Entity type should be specified')); } - return (int)$value['entity_type']; + return $value['entity_type']; } /** @@ -101,13 +103,13 @@ private function getAttributeCode(array $value): string /** * Get attribute options data * - * @param int $entityType + * @param string $entityType * @param string $attributeCode * @return array * @throws GraphQlInputException * @throws GraphQlNoSuchEntityException */ - private function getAttributeOptionsData(int $entityType, string $attributeCode): array + private function getAttributeOptionsData(string $entityType, string $attributeCode): array { try { $optionsData = $this->attributeOptionsDataProvider->getData($entityType, $attributeCode); diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/DataProvider/AttributeOptions.php b/app/code/Magento/EavGraphQl/Model/Resolver/DataProvider/AttributeOptions.php index 900a31c1093ed..3371fbe658c9c 100644 --- a/app/code/Magento/EavGraphQl/Model/Resolver/DataProvider/AttributeOptions.php +++ b/app/code/Magento/EavGraphQl/Model/Resolver/DataProvider/AttributeOptions.php @@ -29,11 +29,13 @@ public function __construct( } /** - * @param int $entityType + * Get attribute options data + * + * @param string $entityType * @param string $attributeCode * @return array */ - public function getData(int $entityType, string $attributeCode): array + public function getData(string $entityType, string $attributeCode): array { $options = $this->optionManager->getItems($entityType, $attributeCode); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeOptionsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeOptionsTest.php new file mode 100644 index 0000000000000..53e09bf590e3c --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeOptionsTest.php @@ -0,0 +1,93 @@ +graphQlQuery($query); + + $expectedOptionArray = [ + [], // description attribute has no options + [ + [ + 'label' => 'Enabled', + 'value' => '1' + ], + [ + 'label' => 'Disabled', + 'value' => '2' + ] + ], + [ + [ + 'label' => 'Option 1', + 'value' => '10' + ], + [ + 'label' => 'Option 2', + 'value' => '11' + ], + [ + 'label' => 'Option 3', + 'value' => '12' + ] + ] + ]; + + $this->assertNotEmpty($response['customAttributeMetadata']['items']); + $actualAttributes = $response['customAttributeMetadata']['items']; + + foreach ($expectedOptionArray as $index => $expectedOptions) { + $actualOption = $actualAttributes[$index]['attribute_options']; + $this->assertEquals($expectedOptions, $actualOption); + } + } +} From 9395f55625c78fe51a96d0300a0a9d1fdc6fc511 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin Date: Wed, 4 Sep 2019 12:21:28 +0300 Subject: [PATCH 054/147] MC-19716: Cannot change action settings of scheduled update for cart rule --- .../Controller/Adminhtml/Promo/Quote/NewActionHtml.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php index 89a0d6e579727..de678851e385c 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php @@ -16,7 +16,7 @@ class NewActionHtml extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote public function execute() { $id = $this->getRequest()->getParam('id'); - $formName = $this->getRequest()->getParam('form'); + $formName = $this->getRequest()->getParam('form_namespace'); $typeArr = explode('|', str_replace('-', '/', $this->getRequest()->getParam('type'))); $type = $typeArr[0]; @@ -37,6 +37,7 @@ public function execute() if ($model instanceof \Magento\Rule\Model\Condition\AbstractCondition) { $model->setJsFormObject($formName); + $model->setFormName($formName); $html = $model->asHtmlRecursive(); } else { $html = ''; From 3557f7794f4bac725615560a04efc3e7865f45b1 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin Date: Wed, 4 Sep 2019 16:54:49 +0300 Subject: [PATCH 055/147] MC-19716: Cannot change action settings of scheduled update for cart rule --- .../Adminhtml/Promo/Quote/NewActionHtml.php | 10 +++-- .../Promo/Quote/NewActionHtmlTest.php | 41 +++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtmlTest.php diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php index de678851e385c..45c0c5fab87ae 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php @@ -1,12 +1,17 @@ setJsFormObject($formName); - $model->setFormName($formName); $html = $model->asHtmlRecursive(); } else { $html = ''; diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtmlTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtmlTest.php new file mode 100644 index 0000000000000..c010ec3e92f74 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtmlTest.php @@ -0,0 +1,41 @@ +getRequest()->setParams( + [ + 'id' => 1, + 'form_namespace' => $formName, + 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product|quote_item_price', + ] + ); + $objectManager = Bootstrap::getObjectManager(); + /** @var NewActionHtml $controller */ + $controller = $objectManager->create(NewActionHtml::class); + $controller->execute(); + $html = $this->getResponse() + ->getBody(); + $this->assertContains($formName, $html); + } +} From 68b4a6be5aa39bb3ceaf678882b0d1dbc09fe8b8 Mon Sep 17 00:00:00 2001 From: Raoul Rego Date: Wed, 4 Sep 2019 11:17:26 -0500 Subject: [PATCH 056/147] MC-17627: Dependency static test does not analyze content of phtml files - Fixed codestyle --- .../TestFramework/Dependency/PhpRule.php | 18 +++++------- .../Dependency/Route/RouteMapper.php | 9 ------ .../Magento/Test/Integrity/DependencyTest.php | 28 ++++++++++--------- .../dependency_test/whitelist/routes_ce.php | 2 +- 4 files changed, 23 insertions(+), 34 deletions(-) diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index 903b1da0ba9ad..ae59e4a017520 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -285,12 +285,11 @@ private function isPluginDependency($dependent, $dependency) * @return array * @throws LocalizedException * @throws \Exception - * @SuppressWarnings(PMD.CyclomaticComplexity) */ protected function _caseGetUrl(string $currentModule, string &$contents): array { $pattern = '#(\->|:)(?getUrl\(([\'"])(?[a-z0-9\-_]{3,}|\*)' - .'(/(?[a-z0-9\-_]+|\*))?(/(?[a-z0-9\-_]+))?\3|\*)#i'; + .'(\/(?[a-z0-9\-_]+|\*))?(\/(?[a-z0-9\-_]+|\*))?\3)#i'; $dependencies = []; if (!preg_match_all($pattern, $contents, $matches, PREG_SET_ORDER)) { @@ -302,18 +301,13 @@ protected function _caseGetUrl(string $currentModule, string &$contents): array $routeId = $item['route_id']; $controllerName = $item['controller_name'] ?? UrlInterface::DEFAULT_CONTROLLER_NAME; $actionName = $item['action_name'] ?? UrlInterface::DEFAULT_ACTION_NAME; - if (in_array( - implode('/', [$routeId, $controllerName, $actionName]), - $this->getRoutesWhitelist() - )) { - continue; - } + // skip rest - if ($routeId == "rest") { //MC-17627 + if ($routeId === "rest") { //MC-17627 continue; } // skip wildcards - if ($routeId == "*" || $controllerName == "*" || $actionName == "*") { //MC-17627 + if ($routeId === "*" || $controllerName === "*" || $actionName === "*") { //MC-17627 continue; } $modules = $this->routeMapper->getDependencyByRoutePath( @@ -333,7 +327,9 @@ protected function _caseGetUrl(string $currentModule, string &$contents): array } } } catch (NoSuchActionException $e) { - throw new LocalizedException(__('Invalid URL path: %1', $e->getMessage()), $e); + if (array_search($e->getMessage(), $this->getRoutesWhitelist()) === false) { + throw new LocalizedException(__('Invalid URL path: %1', $e->getMessage()), $e); + } } return $dependencies; diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php index 8bd6b1479768f..1b93adb5f03ef 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php @@ -243,15 +243,6 @@ private function processConfigFile(string $module, string $configFile) if (!in_array($module, $this->routers[$routerId][$routeId])) { $this->routers[$routerId][$routeId][] = $module; } - if (isset($route['frontName'])) { - $frontName = (string)$route['frontName']; - if (!isset($this->routers[$routerId][$frontName])) { - $this->routers[$routerId][$frontName] = []; - } - if (!in_array($module, $this->routers[$routerId][$frontName])) { - $this->routers[$routerId][$frontName][] = $module; - } - } } } } diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php index d3160be7c72c0..c381aa6b908d8 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php @@ -234,7 +234,8 @@ protected static function _initRules() . '/_files/dependency_test/tables_*.php'; $dbRuleTables = []; foreach (glob($replaceFilePattern) as $fileName) { - $dbRuleTables = array_merge($dbRuleTables, @include $fileName); //phpcs:ignore + //phpcs:ignore Magento2.Performance.ForeachArrayMerge + $dbRuleTables = array_merge($dbRuleTables, @include $fileName); } self::$_rulesInstances = [ new PhpRule( @@ -264,14 +265,14 @@ private static function getRoutesWhitelist(): array { if (is_null(self::$routesWhitelist)) { $routesWhitelistFilePattern = realpath(__DIR__) . '/_files/dependency_test/whitelist/routes_*.php'; - self::$routesWhitelist = array_merge( - ...array_map( - function ($fileName) { - return include $fileName; - }, - glob($routesWhitelistFilePattern) - ) - ); + self::$routesWhitelist = []; + foreach (glob($routesWhitelistFilePattern) as $fileName) { + //phpcs:ignore Magento2.Performance.ForeachArrayMerge + self::$routesWhitelist = array_merge( + self::$routesWhitelist, + include $fileName + ); + } } return self::$routesWhitelist; } @@ -294,7 +295,7 @@ protected function _getCleanedFileContents($fileType, $file) return preg_replace( '~\~s', '', - (string)file_get_contents($file) + file_get_contents($file) ); case 'template': $contents = php_strip_whitespace($file); @@ -310,7 +311,7 @@ function ($matches) use ($contents, &$contentsWithoutHtml) { ); return $contentsWithoutHtml; } - return (string)file_get_contents($file); + return file_get_contents($file); } /** @@ -389,7 +390,8 @@ protected function getDependenciesFromFiles($module, $fileType, $file, $contents foreach (self::$_rulesInstances as $rule) { /** @var \Magento\TestFramework\Dependency\RuleInterface $rule */ $newDependencies = $rule->getDependencyInfo($module, $fileType, $file, $contents); - $dependencies = array_merge($dependencies, $newDependencies); //phpcs:ignore + //phpcs:ignore Magento2.Performance.ForeachArrayMerge + $dependencies = array_merge($dependencies, $newDependencies); } foreach ($dependencies as $key => $dependency) { foreach (self::$whiteList as $namespace) { @@ -504,7 +506,7 @@ public function collectRedundant() foreach (array_keys(self::$mapDependencies) as $module) { $declared = $this->_getDependencies($module, self::TYPE_HARD, self::MAP_TYPE_DECLARED); - //phpcs:ignore + //phpcs:ignore Magento2.Performance.ForeachArrayMerge $found = array_merge( $this->_getDependencies($module, self::TYPE_HARD, self::MAP_TYPE_FOUND), $this->_getDependencies($module, self::TYPE_SOFT, self::MAP_TYPE_FOUND), diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/whitelist/routes_ce.php b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/whitelist/routes_ce.php index 1aef3ffdf104a..9ebc951a3a3a1 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/whitelist/routes_ce.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/whitelist/routes_ce.php @@ -6,5 +6,5 @@ declare(strict_types=1); return [ - 'privacy-policy-cookie-restriction-mode/index/index' + 'privacy-policy-cookie-restriction-mode/index/index', ]; From 656abce59d6fb8743ca6bb4e95805c4cdea34792 Mon Sep 17 00:00:00 2001 From: Raoul Rego Date: Wed, 4 Sep 2019 11:52:27 -0500 Subject: [PATCH 057/147] MC-17627: Dependency static test does not analyze content of phtml files - Fixed codestyle --- .../Magento/TestFramework/Dependency/Route/RouteMapper.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php index 1b93adb5f03ef..315bb2ae26b02 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php @@ -174,7 +174,8 @@ public function getDependencyByRoutePath( $dependencies = []; foreach ($this->getRouterTypes() as $routerId) { if (isset($this->getActionsMap()[$routerId][$routeId][$controllerName][$actionName])) { - $dependencies = array_merge( //phpcs:ignore + //phpcs:ignore Magento2.Performance.ForeachArrayMerge + $dependencies = array_merge( $dependencies, $this->getActionsMap()[$routerId][$routeId][$controllerName][$actionName] ); From 5400b22b97a009fcea62f2a096a260774af1c3a1 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Wed, 4 Sep 2019 13:06:53 -0500 Subject: [PATCH 058/147] MC-18450: Allow custom attributes to be filterable and also returned in the layered navigation the product filter section in GraphQL --- .../LayeredNavigation/Builder/Category.php | 11 +++--- .../Plugin/Search/Request/ConfigReader.php | 2 +- .../CatalogGraphQl/etc/schema.graphqls | 36 +++++++++---------- app/code/Magento/GraphQl/etc/schema.graphqls | 10 +++--- 4 files changed, 31 insertions(+), 28 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php index cebafe31385ba..b0e67d72e25ba 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php @@ -94,10 +94,13 @@ public function build(AggregationInterface $aggregation, ?int $storeId): array if ($this->isBucketEmpty($bucket)) { return []; } - - $categoryIds = \array_map(function (AggregationValueInterface $value) { - return (int)$value->getValue(); - }, $bucket->getValues()); + + $categoryIds = \array_map( + function (AggregationValueInterface $value) { + return (int)$value->getValue(); + }, + $bucket->getValues() + ); $categoryIds = \array_diff($categoryIds, [$this->rootCategoryProvider->getRootCategory($storeId)]); $categoryLabels = \array_column( diff --git a/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php index 02f23e59f15d7..85c18462aae49 100644 --- a/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php +++ b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php @@ -142,7 +142,7 @@ private function generateRequest() case 'static': case 'text': case 'varchar': - if ($attribute->getFrontendInput() === 'multiselect') { + if (in_array($attribute->getFrontendInput(), ['select', 'multiselect'])) { $request['queries'][$queryName] = $this->generateFilterQuery($queryName, $filterName); $request['filters'][$filterName] = $this->generateTermFilter($filterName, $attribute); } elseif ($attribute->getAttributeCode() === 'sku') { diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 8e9471c77dc6b..a2ea18288a77a 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -282,7 +282,7 @@ type CategoryProducts @doc(description: "The category products object returned i } input ProductAttributeFilterInput @doc(description: "ProductAttributeFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { - category_id: FilterEqualTypeInput @doc(description: "Filter product by category id") + category_id: FilterEqualTypeInput @doc(description: "Filter product by category id") } input ProductFilterInput @deprecated(reason: "Attributes used in this input are hardcoded and some of them are not searcheable. Use @ProductAttributeFilterInput instead") @doc(description: "ProductFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { @@ -374,8 +374,8 @@ input ProductSortInput @deprecated(reason: "The attributes used in this input ar input ProductAttributeSortInput @doc(description: "ProductAttributeSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order. It's possible to sort products using searchable attributes with enabled 'Use in Filter Options' option") { - position: SortEnum @doc(description: "The position of products") - relevance: SortEnum @doc(description: "The search relevance score (default)") + relevance: SortEnum @doc(description: "Sort by the search relevance score (default).") + position: SortEnum @doc(description: "Sort by the position assigned to each product.") } type MediaGalleryEntry @doc(description: "MediaGalleryEntry defines characteristics about images and videos associated with a specific product.") { @@ -391,29 +391,29 @@ type MediaGalleryEntry @doc(description: "MediaGalleryEntry defines characterist } type LayerFilter { - name: String @doc(description: "Layered navigation filter name.") @deprecated(reason: "Use Aggregation.label instead") - request_var: String @doc(description: "Request variable name for filter query.") @deprecated(reason: "Use Aggregation.attribute_code instead") - filter_items_count: Int @doc(description: "Count of filter items in filter group.") @deprecated(reason: "Use Aggregation.count instead") - filter_items: [LayerFilterItemInterface] @doc(description: "Array of filter items.") @deprecated(reason: "Use Aggregation.options instead") + name: String @doc(description: "Layered navigation filter name.") @deprecated(reason: "Use Aggregation.label instead.") + request_var: String @doc(description: "Request variable name for filter query.") @deprecated(reason: "Use Aggregation.attribute_code instead.") + filter_items_count: Int @doc(description: "Count of filter items in filter group.") @deprecated(reason: "Use Aggregation.count instead.") + filter_items: [LayerFilterItemInterface] @doc(description: "Array of filter items.") @deprecated(reason: "Use Aggregation.options instead.") } interface LayerFilterItemInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\LayerFilterItemTypeResolverComposite") { - label: String @doc(description: "Filter label.") - value_string: String @doc(description: "Value for filter request variable to be used in query.") - items_count: Int @doc(description: "Count of items by filter.") + label: String @doc(description: "Filter label.") @deprecated(reason: "Use AggregationOption.label instead.") + value_string: String @doc(description: "Value for filter request variable to be used in query.") @deprecated(reason: "Use AggregationOption.value instead.") + items_count: Int @doc(description: "Count of items by filter.") @deprecated(reason: "Use AggregationOption.count instead.") } -type Aggregation { - count: Int @doc(description: "The number of filter items in the filter group.") - label: String @doc(description: "The filter named displayed in layered navigation.") - attribute_code: String! @doc(description: "Attribute code of the filter item.") - options: [AggregationOption] @doc(description: "Describes each aggregated filter option.") +type Aggregation @doc(description: "A bucket that contains information for each filterable option (such as price, category ID, and custom attributes).") { + count: Int @doc(description: "The number of options in the aggregation group.") + label: String @doc(description: "The aggregation display name.") + attribute_code: String! @doc(description: "Attribute code of the aggregation group.") + options: [AggregationOption] @doc(description: "Array of options for the aggregation.") } type AggregationOption { - count: Int @doc(description: "The number of items returned by the filter.") - label: String! @doc(description: "Filter label.") - value: String! @doc(description: "Value for filter request variable to be used in query.") + count: Int @doc(description: "The number of items that match the aggregation option.") + label: String @doc(description: "Aggregation option display label.") + value: String! @doc(description: "The internal ID that represents the value of the option.") } type LayerFilterItem implements LayerFilterItemInterface { diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls index 69b822d4285b8..90a4bd7376e45 100644 --- a/app/code/Magento/GraphQl/etc/schema.graphqls +++ b/app/code/Magento/GraphQl/etc/schema.graphqls @@ -66,17 +66,17 @@ input FilterTypeInput @doc(description: "FilterTypeInput specifies which action } input FilterEqualTypeInput @doc(description: "Specifies which action will be performed in a query ") { - in: [String] - eq: String + in: [String] @doc(description: "In. The value can contain a set of comma-separated values") + eq: String @doc(description: "Equals") } input FilterRangeTypeInput @doc(description: "Specifies which action will be performed in a query ") { - from: String - to: String + from: String @doc(description: "From") + to: String @doc(description: "To") } input FilterMatchTypeInput @doc(description: "Specifies which action will be performed in a query ") { - match: String + match: String @doc(description: "Match. Can be used for fuzzy matching.") } type SearchResultPageInfo @doc(description: "SearchResultPageInfo provides navigation for the query response") { From bc61b81a5075adaee491cab9345ddc301276bc87 Mon Sep 17 00:00:00 2001 From: Raoul Rego Date: Wed, 4 Sep 2019 13:30:47 -0500 Subject: [PATCH 059/147] MC-17627: Dependency static test does not analyze content of phtml files - Fixed codestyle --- .../framework/Magento/TestFramework/Dependency/PhpRule.php | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index ae59e4a017520..33f213454aaa4 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -285,6 +285,7 @@ private function isPluginDependency($dependent, $dependency) * @return array * @throws LocalizedException * @throws \Exception + * @SuppressWarnings(PMD.CyclomaticComplexity) */ protected function _caseGetUrl(string $currentModule, string &$contents): array { From 4e252de786e64d70c69c540c57a25b876c2bd758 Mon Sep 17 00:00:00 2001 From: Raoul Rego Date: Wed, 4 Sep 2019 13:50:02 -0500 Subject: [PATCH 060/147] MC-17627: Dependency static test does not analyze content of phtml files - Fixed codestyle --- .../Magento/Test/Integrity/DependencyTest.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php index c381aa6b908d8..144386df55e3a 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php @@ -286,17 +286,20 @@ private static function getRoutesWhitelist(): array */ protected function _getCleanedFileContents($fileType, $file) { + $contents = null; switch ($fileType) { case 'php': - return php_strip_whitespace($file); + $contents = php_strip_whitespace($file); + break; case 'layout': case 'config': //Removing xml comments - return preg_replace( + $contents = preg_replace( '~\~s', '', file_get_contents($file) ); + break; case 'template': $contents = php_strip_whitespace($file); //Removing html @@ -309,9 +312,12 @@ function ($matches) use ($contents, &$contentsWithoutHtml) { }, $contents ); - return $contentsWithoutHtml; + $contents = $contentsWithoutHtml; + break; + default: + $contents = file_get_contents($file); } - return file_get_contents($file); + return $contents; } /** From b46641504f85905a92aa8be971b9aa93308862d5 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi Date: Wed, 4 Sep 2019 18:51:53 -0500 Subject: [PATCH 061/147] MC-19735: Braintree Paypal order fails in multi address if more than one shipping address is used - Removed shipping address from authorization request for Braintree PayPal Vault since we don't allow a customer to change it on PayPal side. --- .../Request/BillingAddressDataBuilder.php | 112 ++++++++++++++++++ .../Validator/ErrorCodeProviderTest.php | 4 +- .../Braintree/etc/braintree_error_mapping.xml | 1 + app/code/Magento/Braintree/etc/di.xml | 2 +- .../js/view/payment/method-renderer/paypal.js | 18 ++- 5 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 app/code/Magento/Braintree/Gateway/Request/BillingAddressDataBuilder.php diff --git a/app/code/Magento/Braintree/Gateway/Request/BillingAddressDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/BillingAddressDataBuilder.php new file mode 100644 index 0000000000000..403c4d72fe358 --- /dev/null +++ b/app/code/Magento/Braintree/Gateway/Request/BillingAddressDataBuilder.php @@ -0,0 +1,112 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject) + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + + $result = []; + $order = $paymentDO->getOrder(); + + $billingAddress = $order->getBillingAddress(); + if ($billingAddress) { + $result[self::BILLING_ADDRESS] = [ + self::REGION => $billingAddress->getRegionCode(), + self::POSTAL_CODE => $billingAddress->getPostcode(), + self::COUNTRY_CODE => $billingAddress->getCountryId(), + self::FIRST_NAME => $billingAddress->getFirstname(), + self::STREET_ADDRESS => $billingAddress->getStreetLine1(), + self::LAST_NAME => $billingAddress->getLastname(), + self::COMPANY => $billingAddress->getCompany(), + self::EXTENDED_ADDRESS => $billingAddress->getStreetLine2(), + self::LOCALITY => $billingAddress->getCity() + ]; + } + + return $result; + } +} diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ErrorCodeProviderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ErrorCodeProviderTest.php index cddb4852da0e3..605e9253fe2cc 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ErrorCodeProviderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ErrorCodeProviderTest.php @@ -74,9 +74,9 @@ public function getErrorCodeDataProvider(): array 'errors' => [], 'transaction' => [ 'status' => 'processor_declined', - 'processorResponseCode' => '1000' + 'processorResponseCode' => '2059' ], - 'expectedResult' => ['1000'] + 'expectedResult' => ['2059'] ], [ 'errors' => [ diff --git a/app/code/Magento/Braintree/etc/braintree_error_mapping.xml b/app/code/Magento/Braintree/etc/braintree_error_mapping.xml index 7155264b4e6ad..bffcc75705938 100644 --- a/app/code/Magento/Braintree/etc/braintree_error_mapping.xml +++ b/app/code/Magento/Braintree/etc/braintree_error_mapping.xml @@ -21,6 +21,7 @@ Cardholder name is too long. CVV verification failed. CVV verification failed. + Address Verification Failed. Postal code verification failed. Credit card number is prohibited. Addresses must have at least one field filled in. diff --git a/app/code/Magento/Braintree/etc/di.xml b/app/code/Magento/Braintree/etc/di.xml index 6f8b7d1d6c368..150dea8e9fe44 100644 --- a/app/code/Magento/Braintree/etc/di.xml +++ b/app/code/Magento/Braintree/etc/di.xml @@ -381,7 +381,7 @@ Magento\Braintree\Gateway\Request\CustomerDataBuilder Magento\Braintree\Gateway\Request\PaymentDataBuilder Magento\Braintree\Gateway\Request\ChannelDataBuilder - Magento\Braintree\Gateway\Request\AddressDataBuilder + Magento\Braintree\Gateway\Request\BillingAddressDataBuilder Magento\Braintree\Gateway\Request\DescriptorDataBuilder Magento\Braintree\Gateway\Request\StoreConfigBuilder Magento\Braintree\Gateway\Request\MerchantAccountDataBuilder diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js index c46e65ffb8abd..17b233ec251b3 100644 --- a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js @@ -17,7 +17,8 @@ define([ 'Magento_Vault/js/view/payment/vault-enabler', 'Magento_Checkout/js/action/create-billing-address', 'Magento_Braintree/js/view/payment/kount', - 'mage/translate' + 'mage/translate', + 'Magento_Ui/js/model/messageList' ], function ( $, _, @@ -31,7 +32,8 @@ define([ VaultEnabler, createBillingAddress, kount, - $t + $t, + globalMessageList ) { 'use strict'; @@ -413,6 +415,18 @@ define([ */ onVaultPaymentTokenEnablerChange: function () { this.reInitPayPal(); + }, + + /** + * Show error message + * + * @param {String} errorMessage + * @private + */ + showError: function (errorMessage) { + globalMessageList.addErrorMessage({ + message: errorMessage + }); } }); }); From bf5bf0da626628943f69e08201e0798e7c275315 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" Date: Thu, 5 Sep 2019 09:14:15 +0300 Subject: [PATCH 062/147] MC-19669: Product images are not loaded when switching between variations --- .../Magento/Swatches/view/frontend/web/js/swatch-renderer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js index f4b4be7657b8c..a5604adb0945b 100644 --- a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js +++ b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js @@ -754,7 +754,7 @@ define([ $widget.options.jsonConfig.optionPrices ]); - if (checkAdditionalData['update_product_preview_image'] === '1') { + if (parseInt(checkAdditionalData['update_product_preview_image']) === 1) { $widget._loadMedia(); } From 81b740b1d3f4bf88c59bfeb3f63d861951469c72 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin Date: Thu, 5 Sep 2019 10:03:45 +0300 Subject: [PATCH 063/147] MC-19716: Cannot change action settings of scheduled update for cart rule --- .../Adminhtml/Promo/Quote/NewActionHtml.php | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php index 45c0c5fab87ae..56c08864c90c4 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php @@ -6,7 +6,9 @@ namespace Magento\SalesRule\Controller\Adminhtml\Promo\Quote; use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Rule\Model\Condition\AbstractCondition; use Magento\SalesRule\Controller\Adminhtml\Promo\Quote; +use Magento\SalesRule\Model\Rule; /** * New action html action @@ -20,8 +22,10 @@ class NewActionHtml extends Quote implements HttpPostActionInterface */ public function execute() { - $id = $this->getRequest()->getParam('id'); - $formName = $this->getRequest()->getParam('form_namespace'); + $id = $this->getRequest() + ->getParam('id'); + $formName = $this->getRequest() + ->getParam('form_namespace'); $typeArr = explode('|', str_replace('-', '/', $this->getRequest()->getParam('type'))); $type = $typeArr[0]; @@ -32,7 +36,7 @@ public function execute() )->setType( $type )->setRule( - $this->_objectManager->create(\Magento\SalesRule\Model\Rule::class) + $this->_objectManager->create(Rule::class) )->setPrefix( 'actions' ); @@ -40,12 +44,14 @@ public function execute() $model->setAttribute($typeArr[1]); } - if ($model instanceof \Magento\Rule\Model\Condition\AbstractCondition) { + if ($model instanceof AbstractCondition) { $model->setJsFormObject($formName); + $model->setFormName($formName); $html = $model->asHtmlRecursive(); } else { $html = ''; } - $this->getResponse()->setBody($html); + $this->getResponse() + ->setBody($html); } } From 85809dce6ae4684c7433580656e433336e293a28 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" Date: Thu, 5 Sep 2019 12:40:05 +0300 Subject: [PATCH 064/147] MC-19669: Product images are not loaded when switching between variations --- .../Magento/Swatches/view/frontend/web/js/swatch-renderer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js index a5604adb0945b..45c05c7a8db84 100644 --- a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js +++ b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js @@ -754,7 +754,7 @@ define([ $widget.options.jsonConfig.optionPrices ]); - if (parseInt(checkAdditionalData['update_product_preview_image']) === 1) { + if (parseInt(checkAdditionalData['update_product_preview_image'], 10) === 1) { $widget._loadMedia(); } From ec9857daf41c9afe46d65012909c6dd18efe213c Mon Sep 17 00:00:00 2001 From: Serhii Balko Date: Thu, 5 Sep 2019 14:13:14 +0300 Subject: [PATCH 065/147] MC-19623: Redundant entity values --- .../Model/ResourceModel/Entity/Attribute.php | 30 ++++ .../Attribute/Entity/AttributeTest.php | 144 ++++++++++++++++++ .../Catalog/_files/dropdown_attribute.php | 1 - 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/Entity/AttributeTest.php diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php index 0e7a46125d872..8c4b119775e14 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php @@ -457,6 +457,7 @@ protected function _updateAttributeOption($object, $optionId, $option) if (!empty($option['delete'][$optionId])) { if ($intOptionId) { $connection->delete($table, ['option_id = ?' => $intOptionId]); + $this->clearSelectedOptionInEntities($object, $intOptionId); } return false; } @@ -475,6 +476,35 @@ protected function _updateAttributeOption($object, $optionId, $option) return $intOptionId; } + /** + * Clear selected option in entities + * + * @param EntityAttribute|AbstractModel $object + * @param int $optionId + * @return void + */ + private function clearSelectedOptionInEntities($object, $optionId) + { + $backendTable = $object->getBackendTable(); + $attributeId = $object->getAttributeId(); + if (!$backendTable || !$attributeId) { + return; + } + + $where = 'attribute_id = ' . $attributeId; + $update = []; + + if ($object->getBackendType() === 'varchar') { + $where.= " AND FIND_IN_SET('$optionId',value)"; + $update['value'] = new \Zend_Db_Expr("TRIM(BOTH ',' FROM REPLACE(CONCAT(',',value,','),',$optionId,',','))"); + } else { + $where.= ' AND value = ' . $optionId; + $update['value'] = null; + } + + $this->getConnection()->update($backendTable, $update, $where); + } + /** * Save option values records per store * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/Entity/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/Entity/AttributeTest.php new file mode 100644 index 0000000000000..8ecf3da8e1aae --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/Entity/AttributeTest.php @@ -0,0 +1,144 @@ +objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->attributeRepository = $this->objectManager->get(AttributeRepository::class); + $this->model = $this->objectManager->get(Attribute::class); + } + + /** + * Test to Clear selected option in entities after remove + */ + public function testClearSelectedOptionInEntities() + { + $dropdownAttribute = $this->loadAttribute('dropdown_attribute'); + $dropdownOption = array_keys($dropdownAttribute->getOptions())[1]; + + $multiplyAttribute = $this->loadAttribute('multiselect_attribute'); + $multiplyOptions = array_keys($multiplyAttribute->getOptions()); + $multiplySelectedOptions = implode(',', $multiplyOptions); + $multiplyOptionToRemove = $multiplyOptions[1]; + unset($multiplyOptions[1]); + $multiplyOptionsExpected = implode(',', $multiplyOptions); + + $product = $this->loadProduct('simple'); + $product->setData('dropdown_attribute', $dropdownOption); + $product->setData('multiselect_attribute', $multiplySelectedOptions); + $this->productRepository->save($product); + + $product = $this->loadProduct('simple'); + $this->assertEquals( + $dropdownOption, + $product->getData('dropdown_attribute'), + 'The dropdown attribute is not selected' + ); + $this->assertEquals( + $multiplySelectedOptions, + $product->getData('multiselect_attribute'), + 'The multiselect attribute is not selected' + ); + + $this->removeAttributeOption($dropdownAttribute, $dropdownOption); + $this->removeAttributeOption($multiplyAttribute, $multiplyOptionToRemove); + + $product = $this->loadProduct('simple'); + $this->assertEmpty($product->getData('dropdown_attribute')); + $this->assertEquals($multiplyOptionsExpected, $product->getData('multiselect_attribute')); + } + + /** + * Remove option from attribute + * + * @param Attribute $attribute + * @param int $optionId + */ + private function removeAttributeOption(Attribute $attribute, int $optionId): void + { + $removalMarker = [ + 'option' => [ + 'value' => [$optionId => []], + 'delete' => [$optionId => '1'], + ], + ]; + $attribute->addData($removalMarker); + $attribute->save($attribute); + } + + /** + * Load product by sku + * + * @param string $sku + * @return Product + */ + private function loadProduct(string $sku): Product + { + return $this->productRepository->get($sku, true, null, true); + } + + /** + * Load attrubute by code + * + * @param string $attributeCode + * @return Attribute + */ + private function loadAttribute(string $attributeCode): Attribute + { + /** @var Attribute $attribute */ + $attribute = $this->objectManager->create(Attribute::class); + $attribute->loadByCode(4, $attributeCode); + + return $attribute; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute.php index bb7e241d972e5..7077509d622d9 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute.php @@ -37,7 +37,6 @@ 'used_for_sort_by' => 0, 'frontend_label' => ['Drop-Down Attribute'], 'backend_type' => 'varchar', - 'backend_model' => \Magento\Eav\Model\Entity\Attribute\Backend\ArrayBackend::class, 'option' => [ 'value' => [ 'option_1' => ['Option 1'], From 686a71cc69a79a6d48ff2e92e556df3cf5234709 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin Date: Thu, 5 Sep 2019 14:17:18 +0300 Subject: [PATCH 066/147] MC-19716: Cannot change action settings of scheduled update for cart rule --- .../Promo/Quote/NewActionHtmlTest.php | 62 +++++++++++++++---- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtmlTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtmlTest.php index c010ec3e92f74..82f1c53d8f161 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtmlTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtmlTest.php @@ -7,14 +7,30 @@ namespace Magento\SalesRule\Controller\Adminhtml\Promo\Quote; -use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\AbstractBackendController; /** * New action html test + * + * @magentoAppArea adminhtml */ class NewActionHtmlTest extends AbstractBackendController { + /** + * @var string + */ + protected $resource = 'Magento_SalesRule::quote'; + + /** + * @var string + */ + protected $uri = 'backend/sales_rule/promo_quote/newActionHtml'; + + /** + * @var string + */ + private $formName = 'test_form'; + /** * Test verifies that execute method has the proper data-form-part value in html response * @@ -22,20 +38,44 @@ class NewActionHtmlTest extends AbstractBackendController */ public function testExecute(): void { - $formName = 'test_form'; + $this->prepareRequest(); + $this->dispatch($this->uri); + $html = $this->getResponse() + ->getBody(); + $this->assertContains($this->formName, $html); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(); + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(); + parent::testAclNoAccess(); + } + + /** + * Prepare request + * + * @return void + */ + private function prepareRequest(): void + { $this->getRequest()->setParams( [ 'id' => 1, - 'form_namespace' => $formName, + 'form_namespace' => $this->formName, 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product|quote_item_price', ] - ); - $objectManager = Bootstrap::getObjectManager(); - /** @var NewActionHtml $controller */ - $controller = $objectManager->create(NewActionHtml::class); - $controller->execute(); - $html = $this->getResponse() - ->getBody(); - $this->assertContains($formName, $html); + )->setMethod('POST'); } } From 232bbad232ac7c863ad0d77a4058599b24dcd228 Mon Sep 17 00:00:00 2001 From: Serhii Balko Date: Thu, 5 Sep 2019 15:59:21 +0300 Subject: [PATCH 067/147] MC-19623: Redundant entity values --- app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php index 8c4b119775e14..eebe069835c97 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php @@ -496,7 +496,9 @@ private function clearSelectedOptionInEntities($object, $optionId) if ($object->getBackendType() === 'varchar') { $where.= " AND FIND_IN_SET('$optionId',value)"; - $update['value'] = new \Zend_Db_Expr("TRIM(BOTH ',' FROM REPLACE(CONCAT(',',value,','),',$optionId,',','))"); + $update['value'] = new \Zend_Db_Expr( + "TRIM(BOTH ',' FROM REPLACE(CONCAT(',',value,','),',$optionId,',','))" + ); } else { $where.= ' AND value = ' . $optionId; $update['value'] = null; From 1e6389c221fec44aca12090f79cca16fc7db9133 Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh Date: Thu, 5 Sep 2019 16:21:34 +0300 Subject: [PATCH 068/147] MC-19777: Problems editing a specific product attribute --- .../Attribute/Edit/Options/Options.php | 71 +++++++++++++++---- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php index 72f4086c1c56b..7af7bf447c45a 100644 --- a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php +++ b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php @@ -4,16 +4,14 @@ * See COPYING.txt for license details. */ -/** - * Attribute add/edit form options tab - * - * @author Magento Core Team - */ namespace Magento\Eav\Block\Adminhtml\Attribute\Edit\Options; use Magento\Store\Model\ResourceModel\Store\Collection; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; /** + * Attribute add/edit form options tab + * * @api * @since 100.0.2 */ @@ -61,6 +59,7 @@ public function __construct( /** * Is true only for system attributes which use source model + * * Option labels and position for such attributes are kept in source model and thus cannot be overridden * * @return bool @@ -96,12 +95,16 @@ public function getStoresSortedBySortOrder() { $stores = $this->getStores(); if (is_array($stores)) { - usort($stores, function ($storeA, $storeB) { - if ($storeA->getSortOrder() == $storeB->getSortOrder()) { - return $storeA->getId() < $storeB->getId() ? -1 : 1; + usort( + $stores, + function ($storeA, $storeB) { + if ($storeA->getSortOrder() == $storeB->getSortOrder()) { + return $storeA->getId() < $storeB->getId() ? -1 : 1; + } + + return ($storeA->getSortOrder() < $storeB->getSortOrder()) ? -1 : 1; } - return ($storeA->getSortOrder() < $storeB->getSortOrder()) ? -1 : 1; - }); + ); } return $stores; } @@ -130,12 +133,14 @@ public function getOptionValues() } /** - * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute + * Preparing values of attribute options + * + * @param AbstractAttribute $attribute * @param array|\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection $optionCollection * @return array */ protected function _prepareOptionValues( - \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute, + AbstractAttribute $attribute, $optionCollection ) { $type = $attribute->getFrontendInput(); @@ -149,6 +154,41 @@ protected function _prepareOptionValues( $values = []; $isSystemAttribute = is_array($optionCollection); + if ($isSystemAttribute) { + $values = $this->getPreparedValues($optionCollection, $isSystemAttribute, $inputType, $defaultValues); + } else { + $optionCollection->setPageSize(200); + $pageCount = $optionCollection->getLastPageNumber(); + $currentPage = 1; + while ($currentPage <= $pageCount) { + $optionCollection->clear(); + $optionCollection->setCurPage($currentPage); + $values = array_merge( + $values, + $this->getPreparedValues($optionCollection, $isSystemAttribute, $inputType, $defaultValues) + ); + $currentPage++; + } + } + + return $values; + } + + /** + * Return prepared values of system or user defined attribute options + * + * @param array|\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection $optionCollection + * @param bool $isSystemAttribute + * @param string $inputType + * @param array $defaultValues + */ + private function getPreparedValues( + $optionCollection, + bool $isSystemAttribute, + string $inputType, + array $defaultValues + ) { + $values = []; foreach ($optionCollection as $option) { $bunch = $isSystemAttribute ? $this->_prepareSystemAttributeOptionValues( $option, @@ -169,12 +209,13 @@ protected function _prepareOptionValues( /** * Retrieve option values collection + * * It is represented by an array in case of system attribute * - * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute + * @param AbstractAttribute $attribute * @return array|\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection */ - protected function _getOptionValuesCollection(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute) + protected function _getOptionValuesCollection(AbstractAttribute $attribute) { if ($this->canManageOptionDefaultOnly()) { $options = $this->_universalFactory->create( @@ -226,7 +267,7 @@ protected function _prepareSystemAttributeOptionValues($option, $inputType, $def foreach ($this->getStores() as $store) { $storeId = $store->getId(); $value['store' . $storeId] = $storeId == - \Magento\Store\Model\Store::DEFAULT_STORE_ID ? $valuePrefix . $this->escapeHtml($option['label']) : ''; + \Magento\Store\Model\Store::DEFAULT_STORE_ID ? $valuePrefix . $this->escapeHtml($option['label']) : ''; } return [$value]; From 9aa8b75d64c43eec5c9f56b65516052d52ccc6d5 Mon Sep 17 00:00:00 2001 From: Serhii Balko Date: Thu, 5 Sep 2019 18:03:54 +0300 Subject: [PATCH 069/147] MC-19623: Redundant entity values --- .../Model/ResourceModel/Entity/Attribute.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php index eebe069835c97..d05a7e1e2baa4 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php @@ -483,7 +483,7 @@ protected function _updateAttributeOption($object, $optionId, $option) * @param int $optionId * @return void */ - private function clearSelectedOptionInEntities($object, $optionId) + private function clearSelectedOptionInEntities(AbstractModel $object, int $optionId) { $backendTable = $object->getBackendTable(); $attributeId = $object->getAttributeId(); @@ -491,20 +491,24 @@ private function clearSelectedOptionInEntities($object, $optionId) return; } - $where = 'attribute_id = ' . $attributeId; + $connection = $this->getConnection(); + $where = $connection->quoteInto('attribute_id = ?', $attributeId); $update = []; if ($object->getBackendType() === 'varchar') { - $where.= " AND FIND_IN_SET('$optionId',value)"; - $update['value'] = new \Zend_Db_Expr( - "TRIM(BOTH ',' FROM REPLACE(CONCAT(',',value,','),',$optionId,',','))" + $where.= ' AND ' . $connection->prepareSqlCondition('value', ['finset' => $optionId]); + $concat = $connection->getConcatSql(["','", 'value', "','"]); + $expr = $connection->quoteInto( + "TRIM(BOTH ',' FROM REPLACE($concat,',?,',','))", + $optionId ); + $update['value'] = new \Zend_Db_Expr($expr); } else { - $where.= ' AND value = ' . $optionId; + $where.= $connection->quoteInto(' AND value = ?', $optionId); $update['value'] = null; } - $this->getConnection()->update($backendTable, $update, $where); + $connection->update($backendTable, $update, $where); } /** From 2072344ad5196f3c66c6216bdf2a8b2c7f024251 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Thu, 5 Sep 2019 12:17:26 -0500 Subject: [PATCH 070/147] MC-18450: Allow custom attributes to be filterable and also returned in the layered navigation the product filter section in GraphQL - Enable exact search by url_key and sku --- .../Plugin/Search/Request/ConfigReader.php | 55 ++++--------- app/code/Magento/CatalogGraphQl/etc/di.xml | 8 ++ .../CatalogGraphQl/etc/schema.graphqls | 2 +- .../Patch/Data/UpdateUrlKeySearchable.php | 79 +++++++++++++++++++ .../CatalogUrlRewriteGraphQl/etc/di.xml | 8 ++ .../etc/schema.graphqls | 4 + .../FieldMapper/Product/AttributeAdapter.php | 25 +++--- .../Product/FieldProvider/StaticField.php | 9 +-- .../SearchAdapter/Filter/Builder/Term.php | 43 +++++++++- 9 files changed, 172 insertions(+), 61 deletions(-) create mode 100644 app/code/Magento/CatalogUrlRewrite/Setup/Patch/Data/UpdateUrlKeySearchable.php diff --git a/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php index 85c18462aae49..992ab50467c72 100644 --- a/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php +++ b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php @@ -9,7 +9,6 @@ use Magento\CatalogSearch\Model\Search\RequestGenerator; use Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorResolver; use Magento\Eav\Model\Entity\Attribute; -use Magento\Framework\Search\EngineResolverInterface; use Magento\Framework\Search\Request\FilterInterface; use Magento\Framework\Search\Request\QueryInterface; use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; @@ -25,6 +24,9 @@ */ class ConfigReader { + /** Bucket name suffix */ + private const BUCKET_SUFFIX = '_bucket'; + /** * @var string */ @@ -46,26 +48,23 @@ class ConfigReader private $productAttributeCollectionFactory; /** - * @var EngineResolverInterface + * @var array */ - private $searchEngineResolver; - - /** Bucket name suffix */ - private const BUCKET_SUFFIX = '_bucket'; + private $exactMatchAttributes = []; /** * @param GeneratorResolver $generatorResolver * @param CollectionFactory $productAttributeCollectionFactory - * @param EngineResolverInterface $searchEngineResolver + * @param array $exactMatchAttributes */ public function __construct( GeneratorResolver $generatorResolver, CollectionFactory $productAttributeCollectionFactory, - EngineResolverInterface $searchEngineResolver + array $exactMatchAttributes = [] ) { $this->generatorResolver = $generatorResolver; $this->productAttributeCollectionFactory = $productAttributeCollectionFactory; - $this->searchEngineResolver = $searchEngineResolver; + $this->exactMatchAttributes = array_merge($this->exactMatchAttributes, $exactMatchAttributes); } /** @@ -142,12 +141,9 @@ private function generateRequest() case 'static': case 'text': case 'varchar': - if (in_array($attribute->getFrontendInput(), ['select', 'multiselect'])) { + if ($this->isExactMatchAttribute($attribute)) { $request['queries'][$queryName] = $this->generateFilterQuery($queryName, $filterName); $request['filters'][$filterName] = $this->generateTermFilter($filterName, $attribute); - } elseif ($attribute->getAttributeCode() === 'sku') { - $request['queries'][$queryName] = $this->generateFilterQuery($queryName, $filterName); - $request['filters'][$filterName] = $this->generateSkuTermFilter($filterName, $attribute); } else { $request['queries'][$queryName] = $this->generateMatchQuery($queryName, $attribute); } @@ -228,27 +224,6 @@ private function generateTermFilter(string $filterName, Attribute $attribute) ]; } - /** - * Generate term filter for sku field - * - * Sku needs to be treated specially to allow for exact match - * - * @param string $filterName - * @param Attribute $attribute - * @return array - */ - private function generateSkuTermFilter(string $filterName, Attribute $attribute) - { - $field = $this->isElasticSearch() ? 'sku.filter_sku' : 'sku'; - - return [ - 'type' => FilterInterface::TYPE_TERM, - 'name' => $filterName, - 'field' => $field, - 'value' => '$' . $attribute->getAttributeCode() . '$', - ]; - } - /** * Return array representation of query based on filter * @@ -292,16 +267,20 @@ private function generateMatchQuery(string $queryName, Attribute $attribute) } /** - * Check if the current search engine is elasticsearch + * Check if attribute's filter should use exact match * + * @param Attribute $attribute * @return bool */ - private function isElasticSearch() + private function isExactMatchAttribute(Attribute $attribute) { - $searchEngine = $this->searchEngineResolver->getCurrentSearchEngine(); - if (strpos($searchEngine, 'elasticsearch') === 0) { + if (in_array($attribute->getFrontendInput(), ['select', 'multiselect'])) { return true; } + if (in_array($attribute->getAttributeCode(), $this->exactMatchAttributes)) { + return true; + } + return false; } } diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml index 0fe30eb0503ea..485ae792193e3 100644 --- a/app/code/Magento/CatalogGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/di.xml @@ -58,6 +58,14 @@ + + + + sku + + + + diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index a2ea18288a77a..b509865f0e82b 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -285,7 +285,7 @@ input ProductAttributeFilterInput @doc(description: "ProductAttributeFilterInput category_id: FilterEqualTypeInput @doc(description: "Filter product by category id") } -input ProductFilterInput @deprecated(reason: "Attributes used in this input are hardcoded and some of them are not searcheable. Use @ProductAttributeFilterInput instead") @doc(description: "ProductFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { +input ProductFilterInput @doc(description: "ProductFilterInput is deprecated, use @ProductAttributeFilterInput instead. ProductFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { name: FilterTypeInput @doc(description: "The product name. Customers use this name to identify the product.") sku: FilterTypeInput @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") description: FilterTypeInput @doc(description: "Detailed information about the product. The value can include simple HTML tags.") diff --git a/app/code/Magento/CatalogUrlRewrite/Setup/Patch/Data/UpdateUrlKeySearchable.php b/app/code/Magento/CatalogUrlRewrite/Setup/Patch/Data/UpdateUrlKeySearchable.php new file mode 100644 index 0000000000000..75f88a8573069 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Setup/Patch/Data/UpdateUrlKeySearchable.php @@ -0,0 +1,79 @@ +moduleDataSetup = $moduleDataSetup; + $this->categorySetupFactory = $categorySetupFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var CategorySetup $categorySetup */ + $categorySetup = $this->categorySetupFactory->create(['setup' => $this->moduleDataSetup]); + + $categorySetup->updateAttribute( + \Magento\Catalog\Model\Product::ENTITY, + 'url_key', + 'is_searchable', + true + ); + + $categorySetup->updateAttribute( + \Magento\Catalog\Model\Category::ENTITY, + 'url_key', + 'is_searchable', + true + ); + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [CreateUrlAttributes::class]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml index 20e6b7e9c0053..e99f89477e807 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml @@ -14,4 +14,12 @@ + + + + + url_key + + + diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls index 89108e578d673..4453674de04dd 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls @@ -12,6 +12,10 @@ input ProductFilterInput { url_path: FilterTypeInput @deprecated(reason: "Use product's `canonical_url` or url rewrites instead") } +input ProductAttributeFilterInput { + url_key: FilterEqualTypeInput @doc(description: "The part of the URL that identifies the product") +} + input ProductSortInput { url_key: SortEnum @doc(description: "The part of the URL that identifies the product") url_path: SortEnum @deprecated(reason: "Use product's `canonical_url` or url rewrites instead") diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php index c3952b494f2e9..41a50961ae4bc 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php @@ -115,6 +115,18 @@ public function isBooleanType(): bool && $this->getAttribute()->getBackendType() !== 'varchar'; } + /** + * Check if attribute is text type + * + * @return bool + */ + public function isTextType(): bool + { + return in_array($this->getAttribute()->getBackendType(), ['varchar', 'static'], true) + && in_array($this->getFrontendInput(), ['text'], true) + && $this->getAttribute()->getIsVisible(); + } + /** * Check if attribute has boolean type. * @@ -176,19 +188,6 @@ public function getFrontendInput() return $this->getAttribute()->getFrontendInput(); } - /** - * Check if product should always be filterable - * - * @return bool - */ - public function isAlwaysFilterable(): bool - { - // List of attributes which are required to be filterable - $alwaysFilterableAttributes = ['sku']; - - return in_array($this->getAttributeCode(), $alwaysFilterableAttributes, true); - } - /** * Get product attribute instance. * diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php index 466bf5d2d09eb..0f3020974d08a 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php @@ -130,12 +130,9 @@ public function getFields(array $context = []): array ]; } - if ($attributeAdapter->isAlwaysFilterable()) { - $filterFieldName = 'filter_' . $this->fieldNameResolver->getFieldName( - $attributeAdapter, - ['type' => FieldMapperInterface::TYPE_FILTER] - ); - $allAttributes[$fieldName]['fields'][$filterFieldName] = [ + if ($attributeAdapter->isTextType()) { + $keywordFieldName = FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD; + $allAttributes[$fieldName]['fields'][$keywordFieldName] = [ 'type' => $this->fieldTypeConverter->convert( FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD ) diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php b/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php index ed8cd049d2915..d88c7e53d813a 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php @@ -5,10 +5,17 @@ */ namespace Magento\Elasticsearch\SearchAdapter\Filter\Builder; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Search\Request\Filter\Term as TermFilterRequest; use Magento\Framework\Search\Request\FilterInterface as RequestFilterInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface + as FieldTypeConverterInterface; +/** + * Term filter builder + */ class Term implements FilterInterface { /** @@ -16,26 +23,56 @@ class Term implements FilterInterface */ protected $fieldMapper; + /** + * @var AttributeProvider + */ + private $attributeAdapterProvider; + + /** + * @var array + * @see \Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType::$integerTypeAttributes + */ + private $integerTypeAttributes = ['category_ids']; + /** * @param FieldMapperInterface $fieldMapper + * @param AttributeProvider $attributeAdapterProvider + * @param array $integerTypeAttributes */ - public function __construct(FieldMapperInterface $fieldMapper) - { + public function __construct( + FieldMapperInterface $fieldMapper, + AttributeProvider $attributeAdapterProvider = null, + array $integerTypeAttributes = [] + ) { $this->fieldMapper = $fieldMapper; + $this->attributeAdapterProvider = $attributeAdapterProvider + ?? ObjectManager::getInstance()->get(AttributeProvider::class); + $this->integerTypeAttributes = array_merge($this->integerTypeAttributes, $integerTypeAttributes); } /** + * Build term filter request + * * @param RequestFilterInterface|TermFilterRequest $filter * @return array */ public function buildFilter(RequestFilterInterface $filter) { $filterQuery = []; + + $attribute = $this->attributeAdapterProvider->getByAttributeCode($filter->getField()); + $fieldName = $this->fieldMapper->getFieldName($filter->getField()); + + if ($attribute->isTextType() && !in_array($attribute->getAttributeCode(), $this->integerTypeAttributes)) { + $suffix = FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD; + $fieldName .= '.' . $suffix; + } + if ($filter->getValue()) { $operator = is_array($filter->getValue()) ? 'terms' : 'term'; $filterQuery []= [ $operator => [ - $this->fieldMapper->getFieldName($filter->getField()) => $filter->getValue(), + $fieldName => $filter->getValue(), ], ]; } From 7211d50998f786dce0628346439649ac8d196c46 Mon Sep 17 00:00:00 2001 From: Oleksandr Dubovyk Date: Thu, 5 Sep 2019 14:40:08 -0500 Subject: [PATCH 071/147] MC-19440: Bug generating a country list in Admin when Country is restricted - fixed --- .../view/adminhtml/ui_component/customer_address_form.xml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml index 692cb2ecb964d..3af0172b3fca8 100644 --- a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml +++ b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml @@ -191,13 +191,6 @@ text - - - From eb3b3d0b29fdda854a263f68a19e353153a315a1 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Thu, 5 Sep 2019 16:43:28 -0500 Subject: [PATCH 072/147] MC-18450: Allow custom attributes to be filterable and also returned in the layered navigation the product filter section in GraphQL - Update performance tests --- setup/performance-toolkit/benchmark.jmx | 65 +++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/setup/performance-toolkit/benchmark.jmx b/setup/performance-toolkit/benchmark.jmx index d53d59c4f7de8..8ffec6aff0422 100644 --- a/setup/performance-toolkit/benchmark.jmx +++ b/setup/performance-toolkit/benchmark.jmx @@ -38390,7 +38390,7 @@ vars.put("configurable_sku", "Configurable Product - ${__time(YMD)}-${__threadNu false - {"query":"{\n products(\n filter: {\n price: {gt: \"10\"}\n or: {\n sku:{like:\"%Product%\"}\n name:{like:\"%Configurable Product%\"}\n }\n }\n pageSize: 20\n currentPage: 1\n sort: {\n price: ASC\n name:DESC\n }\n ) {\n total_count\n items {\n attribute_set_id\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n short_description {\n html\n }\n sku\n small_image {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n \t... on PhysicalProductInterface {\n \tweight\n \t}\n }\n page_info {\n page_size\n current_page\n }\n }\n}\n","variables":null,"operationName":null} + {"query":"{\n products(\n filter: {\n price: {from: \"5\"}\n name:{match:\"Product\"}\n }\n pageSize: 20\n currentPage: 1\n sort: {\n price: ASC\n name:DESC\n }\n ) {\n total_count\n items {\n attribute_set_id\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n short_description {\n html\n }\n sku\n small_image {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n \t... on PhysicalProductInterface {\n \tweight\n \t}\n }\n page_info {\n page_size\n current_page\n }\n }\n}\n","variables":null,"operationName":null} = @@ -38592,7 +38592,7 @@ if (totalCount == null) { false - {"query":"{\n products(\n pageSize:20\n currentPage:1\n search: \"configurable\"\n filter: {name: {like: \"Configurable Product%\"} }\n ) {\n total_count\n page_info {\n current_page\n page_size\n total_pages\n }\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t\t}\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} + {"query":"{\n products(\n pageSize:20\n currentPage:1\n search: \"configurable\"\n filter: {name: {match: \"Configurable Product\"} }\n ) {\n total_count\n page_info {\n current_page\n page_size\n total_pages\n }\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t\t}\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} = @@ -38631,7 +38631,7 @@ if (totalCount == null) { - String totalCount=vars.get("graphql_search_products_query_total_count_fulltext_filter"); + String totalCount=vars.get("graphql_search_products_query_total_count_fulltext_filter"); if (totalCount == null) { Failure = true; @@ -38660,7 +38660,7 @@ if (totalCount == null) { false - {"query":"{\n products(\n pageSize:20\n currentPage:${graphql_search_products_query_total_pages_fulltext_filter}\n search: \"configurable\"\n filter: {name: {like: \"Configurable Product%\"} }\n ) {\n total_count\n page_info {\n current_page\n page_size\n total_pages\n }\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t\t}\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} + {"query":"{\n products(\n pageSize:20\n currentPage:${graphql_search_products_query_total_pages_fulltext_filter}\n search: \"configurable\"\n filter: {name: {match: \"Configurable Product\"} }\n ) {\n total_count\n page_info {\n current_page\n page_size\n total_pages\n }\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t\t}\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} = @@ -38834,6 +38834,63 @@ if (totalCount == null) { + + true + + + + false + {"query":"{\n products(\n pageSize:20\n currentPage:1\n search: \"Option 1\") {\n aggregations{\n attribute_code\n count\n label\n options{\n count\n label\n value\n }\n }\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n weight\n }\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + + + + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/query_multiple_products_with_extensible_data_objects_using_full_text_and_aggregations.jmx + + + + graphql_search_products_query_total_count + $.data.products.total_count + + + BODY + + + + String totalCount=vars.get("graphql_search_products_query_total_count"); + if (totalCount == null) { + Failure = true; + FailureMessage = "Not Expected \"totalCount\" to be null"; + } else { + if (Integer.parseInt(totalCount) < 1) { + Failure = true; + FailureMessage = "Expected \"totalCount\" to be greater than zero, Actual: " + totalCount; + } else { + Failure = false; + } + } + + + + false + + + + true From 657132a9ee967a074d691674ec3eb47ca9b8607a Mon Sep 17 00:00:00 2001 From: Deepty Thampy Date: Thu, 5 Sep 2019 17:48:01 -0500 Subject: [PATCH 073/147] MC-18514: API functional test to cover filterable custom attributes in layered navigation - fix tests --- .../GraphQl/Catalog/ProductSearchTest.php | 635 +++++++++++++----- ...th_custom_attribute_layered_navigation.php | 14 +- .../_files/dropdown_attribute_rollback.php | 18 + .../_files/products_for_relevance_sorting.php | 75 ++- ...roducts_for_relevance_sorting_rollback.php | 36 + ...th_layered_navigation_custom_attribute.php | 2 +- 6 files changed, 573 insertions(+), 207 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting_rollback.php diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index 8d8969737b7f8..cef036a31e6b7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -13,8 +13,11 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\CategoryLinkManagement; +use Magento\Eav\Model\Config; use Magento\Framework\Config\Data; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Indexer\IndexerInterface; +use Magento\Indexer\Model\Indexer; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; use Magento\Catalog\Model\Product; @@ -32,20 +35,25 @@ class ProductSearchTest extends GraphQlAbstract { /** - * Verify that layered navigation filters are returned for product query + * Verify that layered navigation filters and aggregations are correct for product query * + * Filter products by an array of skus * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testFilterLn() { - CacheCleaner::cleanAll(); + /** @var \Magento\Eav\Model\Config $eavConfig */ + $eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(Config::class); + $attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); + /** @var \Magento\Eav\Api\Data\AttributeOptionInterface[] $options */ + $options = $attribute->getOptions(); $query = <<graphQlQuery($query); $this->assertArrayHasKey( @@ -105,69 +110,111 @@ public function testLayeredNavigationWithConfigurableChildrenOutOfStock() { CacheCleaner::cleanAll(); $attributeCode = 'test_configurable'; + /** @var \Magento\Eav\Model\Config $eavConfig */ - $eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); + $eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(Config::class); $attribute = $eavConfig->getAttribute('catalog_product', $attributeCode); /** @var AttributeOptionInterface[] $options */ $options = $attribute->getOptions(); array_shift($options); $firstOption = $options[0]->getValue(); $secondOption = $options[1]->getValue(); - $query = $this->getQueryProductsWithCustomAttribute($attributeCode, $firstOption); + $query = $this->getQueryProductsWithArrayOfCustomAttributes($attributeCode, $firstOption, $secondOption); + $objectManager = Bootstrap::getObjectManager(); + $indexer = $objectManager->create(Indexer::class); + $indexer->load('catalogsearch_fulltext'); + $indexer->reindexAll(); $response = $this->graphQlQuery($query); - // Out of two children, only one child product of 1st Configurable product with option1 is OOS - $this->assertEquals(1, $response['products']['total_count']); + $this->assertEquals(2, $response['products']['total_count']); + $this->assertNotEmpty($response['products']['aggregations']); + $this->assertNotEmpty($response['products']['filters'],'Filters is empty'); + $this->assertCount(2, $response['products']['aggregations'], 'Aggregation count does not match'); + //$this->assertResponseFields($response['products']['aggregations']) // Custom attribute filter layer data $this->assertResponseFields( - $response['products']['filters'][1], + $response['products']['aggregations'][1], [ - 'name' => $attribute->getDefaultFrontendLabel(), - 'request_var'=> $attribute->getAttributeCode(), - 'filter_items_count'=> 2, - 'filter_items' => [ + 'attribute_code' => $attribute->getAttributeCode(), + 'label'=> $attribute->getDefaultFrontendLabel(), + 'count'=> 2, + 'options' => [ [ 'label' => 'Option 1', - 'items_count' => 1, - 'value_string' => $firstOption, - '__typename' =>'LayerFilterItem' + 'value' => $firstOption, + 'count' =>'2' ], [ 'label' => 'Option 2', - 'items_count' => 1, - 'value_string' => $secondOption, - '__typename' =>'LayerFilterItem' + 'value' => $secondOption, + 'count' =>'2' ] ], ] ); + } - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $productRepository = Bootstrap::getObjectManager()->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); - $outOfStockChildProduct = $productRepository->get('simple_30'); - // All child variations with this attribute are now set to Out of Stock - $outOfStockChildProduct->setStockData( - ['use_config_manage_stock' => 1, - 'qty' => 0, - 'is_qty_decimal' => 0, - 'is_in_stock' => 0] - ); - $productRepository->save($outOfStockChildProduct); - $query = $this->getQueryProductsWithCustomAttribute($attributeCode, $firstOption); - $response = $this->graphQlQuery($query); - $this->assertEquals(0, $response['products']['total_count']); - $this->assertEmpty($response['products']['items']); - $this->assertEmpty($response['products']['filters']); + /** + * + * @return string + */ + private function getQueryProductsWithArrayOfCustomAttributes($attributeCode, $firstOption, $secondOption) : string + { + return <<get('12345'); $product3 = $productRepository->get('simple-4'); $filteredProducts = [$product1, $product2, $product3 ]; + $objectManager = Bootstrap::getObjectManager(); + $indexer = $objectManager->create(Indexer::class); + $indexer->load('catalogsearch_fulltext'); + $indexer->reindexAll(); $response = $this->graphQlQuery($query); $this->assertEquals(3, $response['products']['total_count']); $this->assertTrue(count($response['products']['filters']) > 0, 'Product filters is not empty'); $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); - // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall + //phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall for ($itemIndex = 0; $itemIndex < count($filteredProducts); $itemIndex++) { $this->assertNotEmpty($productItemsInResponse[$itemIndex]); //validate that correct products are returned @@ -234,33 +296,35 @@ public function testAdvancedSearchByOneCustomAttribute() /** @var \Magento\Eav\Model\Config $eavConfig */ $eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); $attribute = $eavConfig->getAttribute('catalog_product', 'second_test_configurable'); - - // Validate custom attribute filter layer data + // Validate custom attribute filter layer data from aggregations $this->assertResponseFields( - $response['products']['filters'][2], + $response['products']['aggregations'][2], [ - 'name' => $attribute->getDefaultFrontendLabel(), - 'request_var'=> $attribute->getAttributeCode(), - 'filter_items_count'=> 1, - 'filter_items' => [ + 'attribute_code' => $attribute->getAttributeCode(), + 'count'=> 1, + 'label'=> $attribute->getDefaultFrontendLabel(), + 'options' => [ [ 'label' => 'Option 3', - 'items_count' => 3, - 'value_string' => $optionValue, - '__typename' =>'LayerFilterItem' + 'count' => 3, + 'value' => $optionValue ], ], ] ); } /** - * Filter products using custom attribute of input type select(dropdown) and filterTypeInput eq + * Filter products using an array of multi select custom attributes * * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testFilterProductsByMultiSelectCustomAttribute() + public function testFilterProductsByMultiSelectCustomAttributes() { + $objectManager = Bootstrap::getObjectManager(); + $indexer = $objectManager->create(Indexer::class); + $indexer->load('catalogsearch_fulltext'); + $indexer->reindexAll(); CacheCleaner::cleanAll(); $attributeCode = 'multiselect_attribute'; /** @var \Magento\Eav\Model\Config $eavConfig */ @@ -304,8 +368,18 @@ public function testFilterProductsByMultiSelectCustomAttribute() value_string __typename } - - } + } + aggregations{ + attribute_code + count + label + options + { + label + value + + } + } } } @@ -314,6 +388,7 @@ public function testFilterProductsByMultiSelectCustomAttribute() $response = $this->graphQlQuery($query); $this->assertEquals(3, $response['products']['total_count']); $this->assertNotEmpty($response['products']['filters']); + $this->assertNotEmpty($response['products']['aggregations']); } /** @@ -335,22 +410,27 @@ private function getDefaultAttributeOptionValue(string $attributeCode) : string } /** - * Full text search for Product and then filter the results by custom attribute + * Full text search for Products and then filter the results by custom attribute ( sort is by defaulty by relevance) * * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testFullTextSearchForProductAndFilterByCustomAttribute() + public function testSearchAndFilterByCustomAttribute() { + $objectManager = Bootstrap::getObjectManager(); + $indexer = $objectManager->create(Indexer::class); + $indexer->load('catalogsearch_fulltext'); + $indexer->reindexAll(); CacheCleaner::cleanAll(); - $optionValue = $this->getDefaultAttributeOptionValue('second_test_configurable'); + $attribute_code = 'second_test_configurable'; + $optionValue = $this->getDefaultAttributeOptionValue($attribute_code); $query = <<graphQlQuery($query); //Verify total count of the products returned $this->assertEquals(3, $response['products']['total_count']); + $this->assertArrayHasKey('filters', $response['products']); + $this->assertCount(3, $response['products']['aggregations']); $expectedFilterLayers = [ - ['name' => 'Price', - 'request_var'=> 'price' - ], ['name' => 'Category', - 'request_var'=> 'category_id' + 'request_var'=> 'cat' ], ['name' => 'Second Test Configurable', - 'request_var'=> 'second_test_configurable' - ], + 'request_var'=> 'second_test_configurable' + ] ]; $layers = array_map(null, $expectedFilterLayers, $response['products']['filters']); - //Verify all the three layers : Price, Category and Custom attribute layers are created + //Verify all the three layers from filters : Price, Category and Custom attribute layers foreach ($layers as $layerIndex => $layerFilterData) { $this->assertNotEmpty($layerFilterData); $this->assertEquals( @@ -415,39 +506,74 @@ public function testFullTextSearchForProductAndFilterByCustomAttribute() ); } - // Validate the price filter layer data from the response + // Validate the price layer of aggregations from the response $this->assertResponseFields( - $response['products']['filters'][0], + $response['products']['aggregations'][0], [ - 'name' => 'Price', - 'request_var'=> 'price', - 'filter_items_count'=> 2, - 'filter_items' => [ + 'attribute_code' => 'price', + 'count'=> 2, + 'label'=> 'Price', + 'options' => [ [ + 'count' => 2, 'label' => '10-20', - 'items_count' => 2, - 'value_string' => '10_20', - '__typename' =>'LayerFilterItem' + 'value' => '10_20', + ], - [ - 'label' => '40-*', - 'items_count' => 1, - 'value_string' => '40_*', - '__typename' =>'LayerFilterItem' - ], + [ + 'count' => 1, + 'label' => '40-*', + 'value' => '40_*', + + ], ], ] ); + // Validate the custom attribute layer of aggregations from the response + $this->assertResponseFields( + $response['products']['aggregations'][2], + [ + 'attribute_code' => $attribute_code, + 'count'=> 1, + 'label'=> 'Second Test Configurable', + 'options' => [ + [ + 'count' => 3, + 'label' => 'Option 3', + 'value' => $optionValue, + + ] + + ], + ] + ); + // 7 categories including the subcategories to which the items belong to , are returned + $this->assertCount(7, $response['products']['aggregations'][1]['options']); + unset($response['products']['aggregations'][1]['options']); + $this->assertResponseFields( + $response['products']['aggregations'][1], + [ + 'attribute_code' => 'category_id', + 'count'=> 7, + 'label'=> 'Category' + ] + ); } /** - * Filter by single category and custom attribute + * Filter by category and custom attribute * * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testFilterByCategoryIdAndCustomAttribute() { + $objectManager = Bootstrap::getObjectManager(); + $indexer = $objectManager->create(Indexer::class); + $indexer->load('catalogsearch_fulltext'); + $indexer->reindexAll(); + CacheCleaner::cleanAll(); + $categoryId = 13; $optionValue = $this->getDefaultAttributeOptionValue('second_test_configurable'); $query = <<graphQlQuery($query); $this->assertEquals(2, $response['products']['total_count']); - $actualCategoryFilterItems = $response['products']['filters'][1]['filter_items']; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + $product1 = $productRepository->get('simple-4'); + $product2 = $productRepository->get('simple'); + $filteredProducts = [$product1, $product2]; + $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); + //phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall + for ($itemIndex = 0; $itemIndex < count($filteredProducts); $itemIndex++) { + $this->assertNotEmpty($productItemsInResponse[$itemIndex]); + //validate that correct products are returned + $this->assertResponseFields( + $productItemsInResponse[$itemIndex][0], + [ 'name' => $filteredProducts[$itemIndex]->getName(), + 'sku' => $filteredProducts[$itemIndex]->getSku() + ] + ); + } + $this->assertNotEmpty($response['products']['filters'],'filters is empty'); + $this->assertNotEmpty($response['products']['aggregations'], 'Aggregations should not be empty'); + $this->assertCount(3, $response['products']['aggregations']); + + $actualCategoriesFromResponse = $response['products']['aggregations'][1]['options']; + //Validate the number of categories/sub-categories that contain the products with the custom attribute - $this->assertCount(6, $actualCategoryFilterItems); + $this->assertCount(6, $actualCategoriesFromResponse); - $expectedCategoryFilterItems = + $expectedCategoryInAggregrations = [ - [ 'label' => 'Category 1', - 'items_count'=> 2 + [ + 'count' => 2, + 'label' => 'Category 1', + 'value'=> '3' ], - [ 'label' => 'Category 1.1', - 'items_count'=> 1 + [ + 'count'=> 1, + 'label' => 'Category 1.1', + 'value'=> '4' + ], - [ 'label' => 'Movable Position 2', - 'items_count'=> 1 + [ + 'count'=> 1, + 'label' => 'Movable Position 2', + 'value'=> '10' + ], - [ 'label' => 'Movable Position 3', - 'items_count'=> 1 + [ + 'count'=> 1, + 'label' => 'Movable Position 3', + 'value'=> '11' ], - [ 'label' => 'Category 12', - 'items_count'=> 1 + [ + 'count'=> 1, + 'label' => 'Category 12', + 'value'=> '12' + ], - [ 'label' => 'Category 1.2', - 'items_count'=> 2 + [ + 'count'=> 2, + 'label' => 'Category 1.2', + 'value'=> '13' ], ]; - $categoryFilterItems = array_map(null, $expectedCategoryFilterItems, $actualCategoryFilterItems); + $categoryInAggregations = array_map(null, $expectedCategoryInAggregrations, $actualCategoriesFromResponse); //Validate the categories and sub-categories data in the filter layer - foreach ($categoryFilterItems as $index => $categoryFilterData) { - $this->assertNotEmpty($categoryFilterData); + foreach ($categoryInAggregations as $index => $categoryAggregationsData) { + $this->assertNotEmpty($categoryAggregationsData); $this->assertEquals( - $categoryFilterItems[$index][0]['label'], - $actualCategoryFilterItems[$index]['label'], + $categoryInAggregations[$index][0]['label'], + $actualCategoriesFromResponse[$index]['label'], 'Category is incorrect' ); $this->assertEquals( - $categoryFilterItems[$index][0]['items_count'], - $actualCategoryFilterItems[$index]['items_count'], + $categoryInAggregations[$index][0]['count'], + $actualCategoriesFromResponse[$index]['count'], 'Products count in the category is incorrect' ); } @@ -567,8 +740,18 @@ private function getQueryProductsWithCustomAttribute($attributeCode, $optionValu value_string __typename } + - } + } + aggregations{ + attribute_code + count + label + options{ + label + value + } + } } } @@ -589,32 +772,15 @@ private function getExpectedFiltersDataSet() $options = $attribute->getOptions(); // Fetching option ID is required for continuous debug as of autoincrement IDs. return [ - [ - 'name' => 'Price', - 'filter_items_count' => 2, - 'request_var' => 'price', - 'filter_items' => [ - [ - 'label' => '*-10', - 'value_string' => '*_10', - 'items_count' => 1, - ], - [ - 'label' => '10-*', - 'value_string' => '10_*', - 'items_count' => 1, - ], - ], - ], [ 'name' => 'Category', 'filter_items_count' => 1, - 'request_var' => 'category_id', + 'request_var' => 'cat', 'filter_items' => [ [ 'label' => 'Category 1', 'value_string' => '333', - 'items_count' => 2, + 'items_count' => 3, ], ], ], @@ -629,7 +795,24 @@ private function getExpectedFiltersDataSet() 'items_count' => 1, ], ], - ] + ], + [ + 'name' => 'Price', + 'filter_items_count' => 2, + 'request_var' => 'price', + 'filter_items' => [ + [ + 'label' => '$0.00 - $9.99', + 'value_string' => '-10', + 'items_count' => 1, + ], + [ + 'label' => '$10.00 and above', + 'value_string' => '10-', + 'items_count' => 1, + ], + ], + ], ]; } @@ -643,8 +826,8 @@ private function getExpectedFiltersDataSet() private function assertFilters($response, $expectedFilters, $message = '') { $this->assertArrayHasKey('filters', $response['products'], 'Product has filters'); - $this->assertTrue(is_array(($response['products']['filters'])), 'Product filters is array'); - $this->assertTrue(count($response['products']['filters']) > 0, 'Product filters is not empty'); + $this->assertTrue(is_array(($response['products']['filters'])), 'Product filters is not array'); + $this->assertTrue(count($response['products']['filters']) > 0, 'Product filters is empty'); foreach ($expectedFilters as $expectedFilter) { $found = false; foreach ($response['products']['filters'] as $responseFilter) { @@ -661,7 +844,7 @@ private function assertFilters($response, $expectedFilters, $message = '') } /** - * Verify that items between the price range of 5 and 50 are returned after sorting name in DESC + * Verify product filtering using price range AND matching skus AND name sorted in DESC order * * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -675,8 +858,8 @@ public function testFilterProductsWithinSpecificPriceRangeSortedByNameDesc() filter: { price:{from: "5", to: "50"} - sku:{like:"simple%"} - name:{like:"Simple%"} + sku:{in:["simple1", "simple2"]} + name:{match:"Simple"} } pageSize:4 currentPage:1 @@ -798,7 +981,7 @@ public function testSearchWithFilterWithPageSizeEqualTotalCount() * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testQueryProductsInCurrentPageSortedByMultipleSortParameters() + public function testFilterByMultipleFilterFieldsSortedByMultipleSortFields() { $query = <<get(ProductRepositoryInterface::class); - $product = $productRepository->get('simple2'); - + $product1 = $productRepository->get('grey_shorts'); + $product2 = $productRepository->get('white_shorts'); $response = $this->graphQlQuery($query); $this->assertEquals(2, $response['products']['total_count']); - $this->assertEquals(['page_size' => 1, 'current_page' => 2], $response['products']['page_info']); - $this->assertEquals( - [['sku' => $product->getSku(), 'name' => $product->getName()]], + $this->assertEquals(['page_size' => 2, 'current_page' => 1], $response['products']['page_info']); + $this->assertEquals + ( + [ + ['sku' => $product1->getSku(), 'name' => $product1->getName()], + ['sku' => $product2->getSku(), 'name' => $product2->getName()] + ], $response['products']['items'] ); + $this->assertArrayHasKey('aggregations', $response['products']); + $this->assertCount(2, $response['products']['aggregations']); + $expectedAggregations =[ + [ + 'attribute_code' => 'price', + 'count' => 2, + 'label' => 'Price', + 'options' => [ + [ + 'label' => '10-20', + 'value' => '10_20', + 'count' => 1, + ], + [ + 'label' => '20-*', + 'value' => '20_*', + 'count' => 1, + ] + ] + ], + [ + 'attribute_code' => 'category_id', + 'count' => 1, + 'label' => 'Category', + 'options' => [ + [ + 'label' => 'Colorful Category', + 'value' => '330', + 'count' => 2, + ], + ], + ] + ]; + $this->assertEquals($expectedAggregations, $response['products']['aggregations']); } /** @@ -1060,12 +1291,19 @@ public function testFilterProductsBySingleCategoryId() /** * Sorting the search results by relevance (DESC => most relevant) * + * Search for products for a fuzzy match and checks if all matching results returned including + * results based on matching keywords from description + * * @magentoApiDataFixture Magento/Catalog/_files/products_for_relevance_sorting.php * @return void */ - public function testFilterProductsAndSortByRelevance() + public function testSearchAndSortByRelevance() { - $search_term ="red white blue grey socks"; + $objectManager = Bootstrap::getObjectManager(); + $indexer = $objectManager->create(Indexer::class); + $indexer->load('catalogsearch_fulltext'); + $indexer->reindexAll(); + $search_term ="blue"; $query = <<graphQlQuery($query); - $this->assertEquals(2, $response['products']['total_count']); + $this->assertEquals(4, $response['products']['total_count']); + $this->assertNotEmpty($response['products']['filters'],'Filters should have the Category layer'); + $this->assertEquals('Colorful Category', $response['products']['filters'][0]['filter_items'][0]['label']); + $productsInResponse = ['Blue briefs', 'ocean blue Shoes', 'Navy Striped Shoes','Grey shorts']; + for ($i = 0; $i < count($response['products']['items']); $i++) { + $this->assertEquals($productsInResponse[$i], $response['products']['items'][$i]['name']); + } } /** - * Sorting by price in the DESC order from the filtered items with default pageSize + * Filtering for product with sku "equals" a specific value + * If pageSize and current page are not requested, default values are returned * * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testQuerySortByPriceDESCWithDefaultPageSize() + public function testFilterByExactSkuAndSortByPriceDesc() { $query = <<get(ProductRepositoryInterface::class); $visibleProduct1 = $productRepository->get('simple1'); - $visibleProduct2 = $productRepository->get('simple2'); - $filteredProducts = [$visibleProduct2, $visibleProduct1]; + + $filteredProducts = [$visibleProduct1]; $response = $this->graphQlQuery($query); - $this->assertEquals(2, $response['products']['total_count']); + $this->assertEquals(1, $response['products']['total_count']); $this->assertProductItems($filteredProducts, $response); $this->assertEquals(20, $response['products']['page_info']['page_size']); $this->assertEquals(1, $response['products']['page_info']['current_page']); } /** - * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php + * Fuzzy search filtered for price and sorted by price and name + * + * @magentoApiDataFixture Magento/Catalog/_files/products_for_relevance_sorting.php */ public function testProductBasicFullTextSearchQuery() { - $textToSearch = 'Simple'; + $objectManager = Bootstrap::getObjectManager(); + $indexer = $objectManager->create(Indexer::class); + $indexer->load('catalogsearch_fulltext'); + $indexer->reindexAll(); + $textToSearch = 'blue'; $query =<<get(ProductRepositoryInterface::class); - $prod1 = $productRepository->get('simple1'); + $prod1 = $productRepository->get('blue_briefs'); $response = $this->graphQlQuery($query); $this->assertEquals(1, $response['products']['total_count']); @@ -1234,6 +1516,8 @@ public function testProductBasicFullTextSearchQuery() } /** + * Filter products purely in a given price range + * * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php */ @@ -1242,8 +1526,8 @@ public function testFilterProductsWithinASpecificPriceRangeSortedByPriceDESC() /** @var ProductRepositoryInterface $productRepository */ $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - $prod1 = $productRepository->get('simple2'); - $prod2 = $productRepository->get('simple1'); + $prod1 = $productRepository->get('simple1'); + $prod2 = $productRepository->get('simple2'); $filteredProducts = [$prod1, $prod2]; /** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ $categoryLinkManagement = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() @@ -1268,7 +1552,7 @@ public function testFilterProductsWithinASpecificPriceRangeSortedByPriceDESC() currentPage:1 sort: { - price:DESC + price:ASC } ) { @@ -1322,7 +1606,7 @@ public function testFilterProductsWithinASpecificPriceRangeSortedByPriceDESC() $this->assertEquals(2, $response['products']['total_count']); $this->assertProductItemsWithPriceCheck($filteredProducts, $response); //verify that by default Price and category are the only layers available - $filterNames = ['Price', 'Category']; + $filterNames = ['Category', 'Price']; $this->assertCount(2, $response['products']['filters'], 'Filter count does not match'); for ($i = 0; $i < count($response['products']['filters']); $i++) { $this->assertEquals($filterNames[$i], $response['products']['filters'][$i]['name']); @@ -1344,8 +1628,8 @@ public function testQueryFilterNoMatchingItems() filter: { price:{from:"50"} - sku:{like:"simple%"} - name:{like:"simple%"} + + description:{match:"Description"} } pageSize:2 @@ -1449,6 +1733,7 @@ public function testQueryPageOutOfBoundException() } /** + * No filter or search arguments used * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testQueryWithNoSearchOrFilterArgumentException() @@ -1494,7 +1779,7 @@ public function testFilterProductsThatAreOutOfStockWithConfigSettings() products( filter: { - sku:{like:"simple%"} + sku:{eq:"simple_visible_in_stock"} } pageSize:20 @@ -1547,7 +1832,7 @@ public function testInvalidCurrentPage() products ( filter: { sku: { - like:"simple%" + eq:"simple1" } } pageSize: 4 @@ -1576,7 +1861,7 @@ public function testInvalidPageSize() products ( filter: { sku: { - like:"simple%" + eq:"simple2" } } pageSize: 0 diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php index 03a4baabf088e..27564d486c808 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php @@ -10,6 +10,7 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\TestFramework\Helper\CacheCleaner; $eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); @@ -27,18 +28,7 @@ /** @var AttributeRepositoryInterface $attributeRepository */ $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); $attributeRepository->save($attribute); - -/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ -$productRepository = Bootstrap::getObjectManager()->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); -$outOfStockChildProduct = $productRepository->get('simple_10'); -$outOfStockChildProduct->setStockData( - ['use_config_manage_stock' => 1, - 'qty' => 0, - 'is_qty_decimal' => 0, - 'is_in_stock' => 0] -); -$productRepository->save($outOfStockChildProduct); - +CacheCleaner::cleanAll(); /** @var \Magento\Indexer\Model\Indexer\Collection $indexerCollection */ $indexerCollection = Bootstrap::getObjectManager()->get(\Magento\Indexer\Model\Indexer\Collection::class); $indexerCollection->load(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute_rollback.php new file mode 100644 index 0000000000000..0ed7317762056 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute_rollback.php @@ -0,0 +1,18 @@ +get('Magento\Framework\Registry'); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +$attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + 'Magento\Catalog\Model\ResourceModel\Eav\Attribute' +); +$attribute->load('dropdown_attribute', 'attribute_code'); +$attribute->delete(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting.php index b8bdda92bc308..56f7d97e8c0fc 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting.php @@ -6,10 +6,26 @@ declare(strict_types=1); // phpcs:ignore Magento2.Security.IncludeFile -require __DIR__ . '/../../Framework/Search/_files/products.php'; +include __DIR__ . '/../../Framework/Search/_files/products.php'; use Magento\Catalog\Api\ProductRepositoryInterface; $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$categoryLinkRepository = $objectManager->create( + \Magento\Catalog\Api\CategoryLinkRepositoryInterface::class, + [ + 'productRepository' => $productRepository + ] +); +$categoryLinkManagement = $objectManager->create( + \Magento\Catalog\Api\CategoryLinkManagementInterface::class, + [ + 'productRepository' => $productRepository, + 'categoryLinkRepository' => $categoryLinkRepository + ] +); $category = $objectManager->create(\Magento\Catalog\Model\Category::class); $category->isObjectNew(true); $category->setId( @@ -21,7 +37,7 @@ )->setParentId( 2 )->setPath( - '1/2/300' + '1/2/330' )->setLevel( 2 )->setAvailableSortBy( @@ -45,31 +61,52 @@ ->setAttributeSetId($defaultAttributeSet) ->setStoreId(1) ->setWebsiteIds([1]) - ->setName('Red White and Blue striped Shoes') - ->setSku('red white and blue striped shoes') + ->setName('Navy Striped Shoes') + ->setSku('navy-striped-shoes') ->setPrice(40) ->setWeight(8) ->setDescription('Red white and blue flip flops at one') - ->setMetaTitle('Multi colored shoes meta title') - ->setMetaKeyword('red, white,flip-flops, women, kids') - ->setMetaDescription('flip flops women kids meta description') + ->setMetaTitle('navy colored shoes meta title') + ->setMetaKeyword('navy, striped , women, kids') + ->setMetaDescription('shoes women kids meta description') ->setStockData(['use_config_manage_stock' => 0]) ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) ->save(); -/** @var ProductRepositoryInterface $productRepository */ -$productRepository = $objectManager->get(ProductRepositoryInterface::class); -$skus = ['green_socks', 'white_shorts','red_trousers','blue_briefs','grey_shorts', 'red white and blue striped shoes' ]; -$products = []; -foreach ($skus as $sku) { - $products = $productRepository->get($sku); -} + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($defaultAttributeSet) + ->setStoreId(1) + ->setWebsiteIds([1]) + ->setName('ocean blue Shoes') + ->setSku('ocean-blue-shoes') + ->setPrice(40) + ->setWeight(8) + ->setDescription('light blue shoes one') + ->setMetaTitle('light blue shoes meta title') + ->setMetaKeyword('light, blue , women, kids') + ->setMetaDescription('shoes women kids meta description') + ->setStockData(['use_config_manage_stock' => 0]) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->save(); + +/** @var \Magento\Catalog\Model\Product $greyProduct */ +$greyProduct = $productRepository->get('grey_shorts'); +$greyProduct->setDescription('Description with blue lines'); +$productRepository->save($greyProduct); + +$skus = ['green_socks', 'white_shorts','red_trousers','blue_briefs','grey_shorts', + 'navy-striped-shoes', 'ocean-blue-shoes']; + /** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ - $categoryLinkManagement = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); -foreach ($products as $product) { +$categoryLinkManagement = $objectManager->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); +foreach ($skus as $sku) { $categoryLinkManagement->assignProductToCategories( - $product->getSku(), - [300] + $sku, + [330] ); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting_rollback.php new file mode 100644 index 0000000000000..90da9baf2d60e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting_rollback.php @@ -0,0 +1,36 @@ +get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $category \Magento\Catalog\Model\Category */ +$category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Category::class); +$category->load(330); +if ($category->getId()) { + $category->delete(); +} +// Remove products +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productsToDelete = ['green_socks', 'white_shorts','red_trousers','blue_briefs', + 'grey_shorts', 'navy-striped-shoes','ocean-blue-shoes']; + +foreach ($productsToDelete as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + $productRepository->delete($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php index 864a931359bdd..72336c48410d5 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php @@ -59,7 +59,7 @@ 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], 'order' => ['option_0' => 1, 'option_1' => 2], ], - 'default' => ['option_0'] + 'default_value' => 'option_0' ] ); From 433b517b5caad4b74a56b232801d9a5048b89351 Mon Sep 17 00:00:00 2001 From: Buba Suma Date: Thu, 5 Sep 2019 15:04:44 -0500 Subject: [PATCH 074/147] MC-19713: Relative Category links created using PageBuilder have the store name as a URL parameter - Fix category/product link to different store should have a store code in the URL --- .../Magento/Catalog/Block/Widget/Link.php | 20 ++++++++++++ .../Test/Unit/Block/Widget/LinkTest.php | 32 +++++++++++++++---- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/Catalog/Block/Widget/Link.php b/app/code/Magento/Catalog/Block/Widget/Link.php index 3d8a1cdf91ca2..a25af297111d2 100644 --- a/app/code/Magento/Catalog/Block/Widget/Link.php +++ b/app/code/Magento/Catalog/Block/Widget/Link.php @@ -89,12 +89,32 @@ public function getHref() if ($rewrite) { $href = $store->getUrl('', ['_direct' => $rewrite->getRequestPath()]); + + if ($this->addStoreCodeParam($store, $href)) { + $href .= (strpos($href, '?') === false ? '?' : '&') . '___store=' . $store->getCode(); + } } $this->_href = $href; } return $this->_href; } + /** + * Checks whether store code query param should be appended to the URL + * + * @param \Magento\Store\Model\Store $store + * @param string $url + * @return bool + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function addStoreCodeParam(\Magento\Store\Model\Store $store, string $url): bool + { + return $this->getStoreId() + && !$store->isUseStoreInUrl() + && $store->getId() !== $this->_storeManager->getStore()->getId() + && strpos($url, '___store') === false; + } + /** * Parse id_path * diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php index 3ceaf7dd44f57..dcbd3161733aa 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php @@ -141,16 +141,19 @@ public function testGetHrefIfRewriteIsNotFound() * * @dataProvider dataProviderForTestGetHrefWithoutUrlStoreSuffix * @param string $path + * @param int|null $storeId * @param bool $includeStoreCode * @param string $expected * @throws \ReflectionException */ public function testStoreCodeShouldBeIncludedInURLOnlyIfItIsConfiguredSo( string $path, + ?int $storeId, bool $includeStoreCode, string $expected ) { $this->block->setData('id_path', 'entity_type/entity_id'); + $this->block->setData('store_id', $storeId); $objectManager = new ObjectManager($this); $rewrite = $this->createPartialMock(UrlRewrite::class, ['getRequestPath']); @@ -192,17 +195,27 @@ public function testStoreCodeShouldBeIncludedInURLOnlyIfItIsConfiguredSo( $url->expects($this->any()) ->method('getUrl') ->willReturnCallback( - function ($route, $params) use ($store) { - return rtrim($store->getBaseUrl(), '/') .'/'. ltrim($params['_direct'], '/'); + function ($route, $params) use ($storeId) { + $baseUrl = rtrim($this->storeManager->getStore($storeId)->getBaseUrl(), '/'); + return $baseUrl .'/' . ltrim($params['_direct'], '/'); } ); $store->addData(['store_id' => 1, 'code' => 'french']); + $store2 = clone $store; + $store2->addData(['store_id' => 2, 'code' => 'german']); + $this->storeManager ->expects($this->any()) ->method('getStore') - ->willReturn($store); + ->willReturnMap( + [ + [null, $store], + [1, $store], + [2, $store2], + ] + ); $this->urlFinder->expects($this->once()) ->method('findOneByData') @@ -210,7 +223,7 @@ function ($route, $params) use ($store) { [ UrlRewrite::ENTITY_ID => 'entity_id', UrlRewrite::ENTITY_TYPE => 'entity_type', - UrlRewrite::STORE_ID => $store->getStoreId(), + UrlRewrite::STORE_ID => $this->storeManager->getStore($storeId)->getStoreId(), ] ) ->will($this->returnValue($rewrite)); @@ -219,7 +232,7 @@ function ($route, $params) use ($store) { ->method('getRequestPath') ->will($this->returnValue($path)); - $this->assertContains($expected, $this->block->getHref()); + $this->assertEquals($expected, $this->block->getHref()); } /** @@ -255,8 +268,13 @@ public function testGetLabelWithoutCustomText() public function dataProviderForTestGetHrefWithoutUrlStoreSuffix() { return [ - ['/accessories.html', true, 'french/accessories.html'], - ['/accessories.html', false, '/accessories.html'], + ['/accessories.html', null, true, 'french/accessories.html'], + ['/accessories.html', null, false, '/accessories.html'], + ['/accessories.html', 1, true, 'french/accessories.html'], + ['/accessories.html', 1, false, '/accessories.html'], + ['/accessories.html', 2, true, 'german/accessories.html'], + ['/accessories.html', 2, false, '/accessories.html?___store=german'], + ['/accessories.html?___store=german', 2, false, '/accessories.html?___store=german'], ]; } From ef7f98b674892c19dbf0d06f3e788fdd119696df Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin Date: Fri, 6 Sep 2019 11:16:34 +0300 Subject: [PATCH 075/147] MC-19637: Billing and Shipping information disappears after failed AJAX POST request --- app/code/Magento/Checkout/etc/frontend/sections.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/code/Magento/Checkout/etc/frontend/sections.xml b/app/code/Magento/Checkout/etc/frontend/sections.xml index 90c2878f501cf..46dd8d9106545 100644 --- a/app/code/Magento/Checkout/etc/frontend/sections.xml +++ b/app/code/Magento/Checkout/etc/frontend/sections.xml @@ -41,7 +41,6 @@
-
From 17f291b465b5481ffa0ca221593114db01ff8ae2 Mon Sep 17 00:00:00 2001 From: Buba Suma Date: Thu, 5 Sep 2019 14:21:27 -0500 Subject: [PATCH 076/147] MC-19749: Store creation ('app:config:import' with pre-defined stores) fails during upgrade - Fix object manager provider reset causes multiple connection to database --- .../Setup/Model/ObjectManagerProviderTest.php | 31 +++++++++++++--- .../Magento/Framework/Console/Cli.php | 1 + .../Setup/Model/ObjectManagerProvider.php | 5 ++- .../Unit/Model/ObjectManagerProviderTest.php | 35 ++++++++++--------- 4 files changed, 47 insertions(+), 25 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Setup/Model/ObjectManagerProviderTest.php b/dev/tests/integration/testsuite/Magento/Setup/Model/ObjectManagerProviderTest.php index c4fc0fa7c5854..a80da16be67eb 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Model/ObjectManagerProviderTest.php +++ b/dev/tests/integration/testsuite/Magento/Setup/Model/ObjectManagerProviderTest.php @@ -6,9 +6,17 @@ namespace Magento\Setup\Model; +use Magento\Framework\ObjectManagerInterface; use Magento\Setup\Mvc\Bootstrap\InitParamListener; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; +use Symfony\Component\Console\Application; +use Zend\ServiceManager\ServiceLocatorInterface; -class ObjectManagerProviderTest extends \PHPUnit\Framework\TestCase +/** + * Tests ObjectManagerProvider + */ +class ObjectManagerProviderTest extends TestCase { /** * @var ObjectManagerProvider @@ -16,21 +24,34 @@ class ObjectManagerProviderTest extends \PHPUnit\Framework\TestCase private $object; /** - * @var \Zend\ServiceManager\ServiceLocatorInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ServiceLocatorInterface|PHPUnit_Framework_MockObject_MockObject */ private $locator; + /** + * @inheritDoc + */ protected function setUp() { - $this->locator = $this->getMockForAbstractClass(\Zend\ServiceManager\ServiceLocatorInterface::class); + $this->locator = $this->getMockForAbstractClass(ServiceLocatorInterface::class); $this->object = new ObjectManagerProvider($this->locator, new Bootstrap()); + $this->locator->expects($this->any()) + ->method('get') + ->willReturnMap( + [ + [InitParamListener::BOOTSTRAP_PARAM, []], + [Application::class, $this->getMockForAbstractClass(Application::class)], + ] + ); } + /** + * Tests the same instance of ObjectManagerInterface should be provided by the ObjectManagerProvider + */ public function testGet() { - $this->locator->expects($this->once())->method('get')->with(InitParamListener::BOOTSTRAP_PARAM)->willReturn([]); $objectManager = $this->object->get(); - $this->assertInstanceOf(\Magento\Framework\ObjectManagerInterface::class, $objectManager); + $this->assertInstanceOf(ObjectManagerInterface::class, $objectManager); $this->assertSame($objectManager, $this->object->get()); } } diff --git a/lib/internal/Magento/Framework/Console/Cli.php b/lib/internal/Magento/Framework/Console/Cli.php index 2ef41f361027e..34fd6316ce454 100644 --- a/lib/internal/Magento/Framework/Console/Cli.php +++ b/lib/internal/Magento/Framework/Console/Cli.php @@ -93,6 +93,7 @@ public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') } parent::__construct($name, $version); + $this->serviceManager->setService(\Symfony\Component\Console\Application::class, $this); } /** diff --git a/setup/src/Magento/Setup/Model/ObjectManagerProvider.php b/setup/src/Magento/Setup/Model/ObjectManagerProvider.php index e25b976e9207f..79216c8ec89b5 100644 --- a/setup/src/Magento/Setup/Model/ObjectManagerProvider.php +++ b/setup/src/Magento/Setup/Model/ObjectManagerProvider.php @@ -76,10 +76,9 @@ private function createCliCommands() { /** @var CommandListInterface $commandList */ $commandList = $this->objectManager->create(CommandListInterface::class); + $application = $this->serviceLocator->get(Application::class); foreach ($commandList->getCommands() as $command) { - $command->setApplication( - $this->serviceLocator->get(Application::class) - ); + $application->add($command); } } diff --git a/setup/src/Magento/Setup/Test/Unit/Model/ObjectManagerProviderTest.php b/setup/src/Magento/Setup/Test/Unit/Model/ObjectManagerProviderTest.php index 9d40b053e394e..552453c4a185c 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/ObjectManagerProviderTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/ObjectManagerProviderTest.php @@ -47,6 +47,14 @@ public function setUp() public function testGet() { $initParams = ['param' => 'value']; + $commands = [ + new Command('setup:install'), + new Command('setup:upgrade'), + ]; + + $application = $this->getMockBuilder(Application::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); $this->serviceLocatorMock ->expects($this->atLeastOnce()) @@ -56,16 +64,21 @@ public function testGet() [InitParamListener::BOOTSTRAP_PARAM, $initParams], [ Application::class, - $this->getMockBuilder(Application::class)->disableOriginalConstructor()->getMock(), + $application, ], ] ); + $commandListMock = $this->createMock(CommandListInterface::class); + $commandListMock->expects($this->once()) + ->method('getCommands') + ->willReturn($commands); + $objectManagerMock = $this->createMock(ObjectManagerInterface::class); $objectManagerMock->expects($this->once()) ->method('create') ->with(CommandListInterface::class) - ->willReturn($this->getCommandListMock()); + ->willReturn($commandListMock); $objectManagerFactoryMock = $this->getMockBuilder(ObjectManagerFactory::class) ->disableOriginalConstructor() @@ -81,21 +94,9 @@ public function testGet() ->willReturn($objectManagerFactoryMock); $this->assertInstanceOf(ObjectManagerInterface::class, $this->model->get()); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject - */ - private function getCommandListMock() - { - $commandMock = $this->getMockBuilder(Command::class)->disableOriginalConstructor()->getMock(); - $commandMock->expects($this->once())->method('setApplication'); - - $commandListMock = $this->createMock(CommandListInterface::class); - $commandListMock->expects($this->once()) - ->method('getCommands') - ->willReturn([$commandMock]); - return $commandListMock; + foreach ($commands as $command) { + $this->assertSame($application, $command->getApplication()); + } } } From 2f04e666aceba9bcbd863b78613714922a1f0f35 Mon Sep 17 00:00:00 2001 From: Oleksandr Dubovyk Date: Fri, 6 Sep 2019 12:42:04 -0500 Subject: [PATCH 077/147] MC-19798: 'Quote Lifetime (days)' setting does not work - fixed --- app/code/Magento/Sales/Cron/CleanExpiredQuotes.php | 1 - .../Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php b/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php index 021e7b66cd13f..a5c7f71df66c5 100644 --- a/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php +++ b/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php @@ -57,7 +57,6 @@ public function execute() $quotes->addFieldToFilter('store_id', $storeId); $quotes->addFieldToFilter('updated_at', ['to' => date("Y-m-d", time() - $lifetime)]); - $quotes->addFieldToFilter('is_active', 0); foreach ($this->getExpireQuotesAdditionalFilterFields() as $field => $condition) { $quotes->addFieldToFilter($field, $condition); diff --git a/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php b/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php index e424cae85f223..ad6a3e03ba679 100644 --- a/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php +++ b/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php @@ -59,7 +59,7 @@ public function testExecute($lifetimes, $additionalFilterFields) $this->quoteFactoryMock->expects($this->exactly(count($lifetimes))) ->method('create') ->will($this->returnValue($quotesMock)); - $quotesMock->expects($this->exactly((3 + count($additionalFilterFields)) * count($lifetimes))) + $quotesMock->expects($this->exactly((2 + count($additionalFilterFields)) * count($lifetimes))) ->method('addFieldToFilter'); if (!empty($lifetimes)) { $quotesMock->expects($this->exactly(count($lifetimes))) From 41ef34feca73eea48f3dd1744bce529d36909c91 Mon Sep 17 00:00:00 2001 From: Pieter Hoste Date: Sat, 7 Sep 2019 14:34:35 +0200 Subject: [PATCH 078/147] Fixes excluding JS files from bundles when minifying is enabled. --- app/code/Magento/Deploy/Service/Bundle.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Deploy/Service/Bundle.php b/app/code/Magento/Deploy/Service/Bundle.php index f16b93a185595..26e61624c219e 100644 --- a/app/code/Magento/Deploy/Service/Bundle.php +++ b/app/code/Magento/Deploy/Service/Bundle.php @@ -216,7 +216,7 @@ private function isExcluded($filePath, $area, $theme) $excludedFiles = $this->bundleConfig->getExcludedFiles($area, $theme); foreach ($excludedFiles as $excludedFileId) { $excludedFilePath = $this->prepareExcludePath($excludedFileId); - if ($excludedFilePath === $filePath) { + if ($excludedFilePath === $filePath || $excludedFilePath === str_replace('.min.js', '.js', $filePath)) { return true; } } From a7bec1ae86cbc65f7ecef0133272054ed742265a Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi Date: Sun, 8 Sep 2019 19:14:44 -0500 Subject: [PATCH 079/147] MC-19904: Start/End Date time is changed after event saving in event edit page --- .../Framework/Stdlib/DateTime/Timezone.php | 26 +++--- .../Test/Unit/DateTime/TimezoneTest.php | 87 +++++++++++++++---- 2 files changed, 88 insertions(+), 25 deletions(-) diff --git a/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php b/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php index 014854ddd584f..118a3e053bd79 100644 --- a/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php +++ b/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php @@ -195,16 +195,16 @@ public function date($date = null, $locale = null, $useTimezone = true, $include */ public function scopeDate($scope = null, $date = null, $includeTime = false) { - $timezone = $this->_scopeConfig->getValue($this->getDefaultTimezonePath(), $this->_scopeType, $scope); + $timezone = new \DateTimeZone( + $this->_scopeConfig->getValue($this->getDefaultTimezonePath(), $this->_scopeType, $scope) + ); switch (true) { case (empty($date)): - $date = new \DateTime('now', new \DateTimeZone($timezone)); + $date = new \DateTime('now', $timezone); break; case ($date instanceof \DateTime): - $date = $date->setTimezone(new \DateTimeZone($timezone)); - break; case ($date instanceof \DateTimeImmutable): - $date = new \DateTime($date->format('Y-m-d H:i:s'), $date->getTimezone()); + $date = $date->setTimezone($timezone); break; case (!is_numeric($date)): $timeType = $includeTime ? \IntlDateFormatter::SHORT : \IntlDateFormatter::NONE; @@ -212,14 +212,20 @@ public function scopeDate($scope = null, $date = null, $includeTime = false) $this->_localeResolver->getLocale(), \IntlDateFormatter::SHORT, $timeType, - new \DateTimeZone($timezone) + $timezone ); - $date = $formatter->parse($date) ?: (new \DateTime($date))->getTimestamp(); - $date = (new \DateTime(null, new \DateTimeZone($timezone)))->setTimestamp($date); + $timestamp = $formatter->parse($date); + $date = $timestamp + ? (new \DateTime('@' . $timestamp))->setTimezone($timezone) + : new \DateTime($date, $timezone); + break; + case (is_numeric($date)): + $date = new \DateTime('@' . $date); + $date = $date->setTimezone($timezone); break; default: - $date = new \DateTime(is_numeric($date) ? '@' . $date : $date); - $date->setTimezone(new \DateTimeZone($timezone)); + $date = new \DateTime($date, $timezone); + break; } if (!$includeTime) { diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/DateTime/TimezoneTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/DateTime/TimezoneTest.php index 3d7d14a394629..53980e574c267 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/DateTime/TimezoneTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/DateTime/TimezoneTest.php @@ -22,6 +22,16 @@ class TimezoneTest extends \PHPUnit\Framework\TestCase */ private $defaultTimeZone; + /** + * @var string + */ + private $scopeType; + + /** + * @var string + */ + private $defaultTimezonePath; + /** * @var ObjectManager */ @@ -49,6 +59,8 @@ protected function setUp() { $this->defaultTimeZone = date_default_timezone_get(); date_default_timezone_set('UTC'); + $this->scopeType = 'store'; + $this->defaultTimezonePath = 'default/timezone/path'; $this->objectManager = new ObjectManager($this); $this->scopeResolver = $this->getMockBuilder(ScopeResolverInterface::class)->getMock(); @@ -86,9 +98,10 @@ public function testDateIncludeTime($date, $locale, $includeTime, $expectedTimes /** * DataProvider for testDateIncludeTime + * * @return array */ - public function dateIncludeTimeDataProvider() + public function dateIncludeTimeDataProvider(): array { return [ 'Parse d/m/y date without time' => [ @@ -133,9 +146,10 @@ public function testConvertConfigTimeToUtc($date, $configuredTimezone, $expected /** * Data provider for testConvertConfigTimeToUtc + * * @return array */ - public function getConvertConfigTimeToUtcFixtures() + public function getConvertConfigTimeToUtcFixtures(): array { return [ 'string' => [ @@ -181,9 +195,10 @@ public function testDate() /** * DataProvider for testDate + * * @return array */ - private function getDateFixtures() + private function getDateFixtures(): array { return [ 'now_datetime_utc' => [ @@ -239,29 +254,71 @@ private function getTimezone() return new Timezone( $this->scopeResolver, $this->localeResolver, - $this->getMockBuilder(DateTime::class)->getMock(), + $this->createMock(DateTime::class), $this->scopeConfig, - '', - '' + $this->scopeType, + $this->defaultTimezonePath ); } /** * @param string $configuredTimezone + * @param string|null $scope */ - private function scopeConfigWillReturnConfiguredTimezone($configuredTimezone) + private function scopeConfigWillReturnConfiguredTimezone(string $configuredTimezone, string $scope = null) { - $this->scopeConfig->method('getValue')->with('', '', null)->willReturn($configuredTimezone); + $this->scopeConfig->expects($this->atLeastOnce()) + ->method('getValue') + ->with($this->defaultTimezonePath, $this->scopeType, $scope) + ->willReturn($configuredTimezone); } - public function testCheckIfScopeDateSetsTimeZone() + /** + * @dataProvider scopeDateDataProvider + * @param \DateTimeInterface|string|int $date + * @param string $timezone + * @param string $locale + * @param string $expectedDate + */ + public function testScopeDate($date, string $timezone, string $locale, string $expectedDate) { - $scopeDate = new \DateTime('now', new \DateTimeZone('America/Vancouver')); - $this->scopeConfig->method('getValue')->willReturn('America/Vancouver'); + $scopeCode = 'test'; - $this->assertEquals( - $scopeDate->getTimezone(), - $this->getTimezone()->scopeDate(0, $scopeDate->getTimestamp())->getTimezone() - ); + $this->scopeConfigWillReturnConfiguredTimezone($timezone, $scopeCode); + $this->localeResolver->method('getLocale') + ->willReturn($locale); + + $scopeDate = $this->getTimezone()->scopeDate($scopeCode, $date, true); + $this->assertEquals($expectedDate, $scopeDate->format('Y-m-d H:i:s')); + $this->assertEquals($timezone, $scopeDate->getTimezone()->getName()); + } + + /** + * @return array + */ + public function scopeDateDataProvider(): array + { + $utcTz = new \DateTimeZone('UTC'); + + return [ + ['2018-10-20 00:00:00', 'UTC', 'en_US', '2018-10-20 00:00:00'], + ['2018-10-20 00:00:00', 'America/Los_Angeles', 'en_US', '2018-10-20 00:00:00'], + ['2018-10-20 00:00:00', 'Asia/Qatar', 'en_US', '2018-10-20 00:00:00'], + ['10/20/18 00:00', 'UTC', 'en_US', '2018-10-20 00:00:00'], + ['10/20/18 00:00', 'America/Los_Angeles', 'en_US', '2018-10-20 00:00:00'], + ['10/20/18 00:00', 'Asia/Qatar', 'en_US', '2018-10-20 00:00:00'], + ['20/10/18 00:00', 'UTC', 'fr_FR', '2018-10-20 00:00:00'], + ['20/10/18 00:00', 'America/Los_Angeles', 'fr_FR', '2018-10-20 00:00:00'], + ['20/10/18 00:00', 'Asia/Qatar', 'fr_FR', '2018-10-20 00:00:00'], + [1539993600, 'UTC', 'en_US', '2018-10-20 00:00:00'], + [1539993600, 'America/Los_Angeles', 'en_US', '2018-10-19 17:00:00'], + [1539993600, 'Asia/Qatar', 'en_US', '2018-10-20 03:00:00'], + [new \DateTime('2018-10-20', $utcTz), 'UTC', 'en_US', '2018-10-20 00:00:00'], + [new \DateTime('2018-10-20', $utcTz), 'America/Los_Angeles', 'en_US', '2018-10-19 17:00:00'], + [new \DateTime('2018-10-20', $utcTz), 'Asia/Qatar', 'en_US', '2018-10-20 03:00:00'], + [new \DateTimeImmutable('2018-10-20', $utcTz), 'UTC', 'en_US', '2018-10-20 00:00:00'], + [new \DateTimeImmutable('2018-10-20', $utcTz), 'America/Los_Angeles', 'en_US', '2018-10-19 17:00:00'], + [new \DateTimeImmutable('2018-10-20', $utcTz), 'Asia/Qatar', 'en_US', '2018-10-20 03:00:00'], + ]; } } From a356f94dd8004db75b7b57319097cb857fe2c2b9 Mon Sep 17 00:00:00 2001 From: Deepty Thampy Date: Mon, 9 Sep 2019 08:26:37 -0500 Subject: [PATCH 080/147] MC-19702: Add input_type to customAttributeMetadata query - fix test to avoid using hard coded values for attribute options --- .../Catalog/ProductAttributeOptionsTest.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeOptionsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeOptionsTest.php index 53e09bf590e3c..517a1c966b04d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeOptionsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeOptionsTest.php @@ -8,6 +8,7 @@ namespace Magento\GraphQl\Catalog; use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Eav\Api\Data\AttributeOptionInterface; class ProductAttributeOptionsTest extends GraphQlAbstract { @@ -18,6 +19,17 @@ class ProductAttributeOptionsTest extends GraphQlAbstract */ public function testCustomAttributeMetadataOptions() { + /** @var \Magento\Eav\Model\Config $eavConfig */ + $eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); + $attribute = $eavConfig->getAttribute('catalog_product', 'dropdown_attribute'); + /** @var AttributeOptionInterface[] $options */ + $options = $attribute->getOptions(); + array_shift($options); + $optionValues = []; + // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall + for ($i = 0; $i < count($options); $i++) { + $optionValues[] = $options[$i]->getValue(); + } $query = << 'Option 1', - 'value' => '10' + 'value' => $optionValues[0] ], [ 'label' => 'Option 2', - 'value' => '11' + 'value' => $optionValues[1] ], [ 'label' => 'Option 3', - 'value' => '12' + 'value' => $optionValues[2] ] ] ]; From f006daad2981d49026fd392ecce861664d2b7aa3 Mon Sep 17 00:00:00 2001 From: Deepty Thampy Date: Mon, 9 Sep 2019 08:28:19 -0500 Subject: [PATCH 081/147] MC-18514: API functional test to cover filterable custom attributes in layered navigation - fix failing tests --- .../GraphQl/Catalog/ProductSearchTest.php | 62 ++----------------- .../GraphQl/Catalog/ProductViewTest.php | 10 ++- .../GraphQl/Swatches/ProductSearchTest.php | 2 +- 3 files changed, 13 insertions(+), 61 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index cef036a31e6b7..2e3e67c65ca46 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -106,7 +106,7 @@ public function testFilterLn() * @magentoApiDataFixture Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testLayeredNavigationWithConfigurableChildrenOutOfStock() + public function testLayeredNavigationForConfigurableProducts() { CacheCleaner::cleanAll(); $attributeCode = 'test_configurable'; @@ -704,59 +704,6 @@ public function testFilterByCategoryIdAndCustomAttribute() ); } } - /** - * - * @return string - */ - private function getQueryProductsWithCustomAttribute($attributeCode, $optionValue) : string - { - return <<assertEquals(4, $response['products']['total_count']); $this->assertNotEmpty($response['products']['filters'],'Filters should have the Category layer'); $this->assertEquals('Colorful Category', $response['products']['filters'][0]['filter_items'][0]['label']); - $productsInResponse = ['Blue briefs', 'ocean blue Shoes', 'Navy Striped Shoes','Grey shorts']; + $productsInResponse = ['ocean blue Shoes', 'Blue briefs', 'Navy Striped Shoes','Grey shorts']; for ($i = 0; $i < count($response['products']['items']); $i++) { $this->assertEquals($productsInResponse[$i], $response['products']['items'][$i]['name']); } + $this->assertCount(2, $response['products']['aggregations']); } /** @@ -1521,7 +1469,7 @@ public function testProductBasicFullTextSearchQuery() * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php */ - public function testFilterProductsWithinASpecificPriceRangeSortedByPriceDESC() + public function testFilterWithinASpecificPriceRangeSortedByPriceDESC() { /** @var ProductRepositoryInterface $productRepository */ $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php index 378b87fb9591f..5685fcdb25877 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php @@ -981,7 +981,7 @@ public function testProductInAllAnchoredCategories() { $query = << Date: Mon, 9 Sep 2019 09:39:20 -0500 Subject: [PATCH 082/147] MC-18450: Allow custom attributes to be filterable and also returned in the layered navigation the product filter section in GraphQL - Fix sorting issue - Use interface for aggregation options --- .../Product/SearchCriteriaBuilder.php | 16 ++----- .../Model/AggregationOptionTypeResolver.php | 29 ++++++++++++ ...AggregationOptionTypeResolverComposite.php | 45 +++++++++++++++++++ .../Products/DataProvider/ProductSearch.php | 12 ++--- .../Magento/CatalogGraphQl/etc/graphql/di.xml | 7 +++ .../CatalogGraphQl/etc/schema.graphqls | 8 +++- .../Adapter/Mysql/Filter/Preprocessor.php | 5 ++- .../Search/Adapter/Mysql/TemporaryStorage.php | 2 +- 8 files changed, 101 insertions(+), 23 deletions(-) create mode 100644 app/code/Magento/CatalogGraphQl/Model/AggregationOptionTypeResolver.php create mode 100644 app/code/Magento/CatalogGraphQl/Model/AggregationOptionTypeResolverComposite.php diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php index af6ed85196cf8..810e9172d0973 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -97,8 +97,9 @@ public function build(array $args, bool $includeAggregation): SearchCriteriaInte if (!empty($args['search'])) { $this->addFilter($searchCriteria, 'search_term', $args['search']); } - - $this->addDefaultSortOrder($searchCriteria); + if (!$searchCriteria->getSortOrders()) { + $this->addDefaultSortOrder($searchCriteria); + } $this->addVisibilityFilter($searchCriteria, !empty($args['search']), !empty($args['filter'])); $searchCriteria->setCurrentPage($args['currentPage']); @@ -170,21 +171,12 @@ private function addFilter(SearchCriteriaInterface $searchCriteria, string $fiel */ private function addDefaultSortOrder(SearchCriteriaInterface $searchCriteria): void { - $sortOrders = $searchCriteria->getSortOrders() ?? []; - foreach ($sortOrders as $sortOrder) { - // Relevance order is already specified - if ($sortOrder->getField() === 'relevance') { - return; - } - } $defaultSortOrder = $this->sortOrderBuilder ->setField('relevance') ->setDirection(SortOrder::SORT_DESC) ->create(); - $sortOrders[] = $defaultSortOrder; - - $searchCriteria->setSortOrders($sortOrders); + $searchCriteria->setSortOrders([$defaultSortOrder]); } /** diff --git a/app/code/Magento/CatalogGraphQl/Model/AggregationOptionTypeResolver.php b/app/code/Magento/CatalogGraphQl/Model/AggregationOptionTypeResolver.php new file mode 100644 index 0000000000000..3a532a1a6c760 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/AggregationOptionTypeResolver.php @@ -0,0 +1,29 @@ +typeResolvers = $typeResolvers; + } + + /** + * @inheritdoc + */ + public function resolveType(array $data) : string + { + /** @var TypeResolverInterface $typeResolver */ + foreach ($this->typeResolvers as $typeResolver) { + $resolvedType = $typeResolver->resolveType($data); + if ($resolvedType) { + return $resolvedType; + } + } + throw new GraphQlInputException(__('Cannot resolve aggregation option type')); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php index 8c5fe42c730f6..661c5874614b5 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -79,7 +79,7 @@ public function __construct( */ public function getList( SearchCriteriaInterface $searchCriteria, - SearchResultsInterface $searchResult, + SearchResultInterface $searchResult, array $attributes = [] ): SearchResultsInterface { /** @var Collection $collection */ @@ -92,11 +92,11 @@ public function getList( $collection->load(); $this->collectionPostProcessor->process($collection, $attributes); - $searchResult = $this->searchResultsFactory->create(); - $searchResult->setSearchCriteria($searchCriteria); - $searchResult->setItems($collection->getItems()); - $searchResult->setTotalCount($collection->getSize()); - return $searchResult; + $searchResults = $this->searchResultsFactory->create(); + $searchResults->setSearchCriteria($searchCriteria); + $searchResults->setItems($collection->getItems()); + $searchResults->setTotalCount($collection->getSize()); + return $searchResults; } /** diff --git a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml index ea2704f1a4ee5..fe3413dc3b218 100644 --- a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml @@ -28,6 +28,13 @@ + + + + Magento\CatalogGraphQl\Model\AggregationOptionTypeResolver + + + diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index b509865f0e82b..69c13a9e81923 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -403,6 +403,10 @@ interface LayerFilterItemInterface @typeResolver(class: "Magento\\CatalogGraphQl items_count: Int @doc(description: "Count of items by filter.") @deprecated(reason: "Use AggregationOption.count instead.") } +type LayerFilterItem implements LayerFilterItemInterface { + +} + type Aggregation @doc(description: "A bucket that contains information for each filterable option (such as price, category ID, and custom attributes).") { count: Int @doc(description: "The number of options in the aggregation group.") label: String @doc(description: "The aggregation display name.") @@ -410,13 +414,13 @@ type Aggregation @doc(description: "A bucket that contains information for each options: [AggregationOption] @doc(description: "Array of options for the aggregation.") } -type AggregationOption { +interface AggregationOptionInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\AggregationOptionTypeResolverComposite") { count: Int @doc(description: "The number of items that match the aggregation option.") label: String @doc(description: "Aggregation option display label.") value: String! @doc(description: "The internal ID that represents the value of the option.") } -type LayerFilterItem implements LayerFilterItemInterface { +type AggregationOption implements AggregationOptionInterface { } diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php index 37d2dda886259..f164da99292ad 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php @@ -201,8 +201,9 @@ private function processQueryWithField(FilterInterface $filter, $isNegation, $qu ) ->joinLeft( ['current_store' => $table], - 'current_store.attribute_id = main_table.attribute_id AND current_store.store_id = ' - . $currentStoreId, + 'current_store.entity_id = main_table.entity_id AND ' + . 'current_store.attribute_id = main_table.attribute_id AND current_store.store_id = ' + . $currentStoreId, null ) ->columns([$filter->getField() => $ifNullCondition]) diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorage.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorage.php index 60ee2d5706067..7f8ef8c422b92 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorage.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorage.php @@ -152,7 +152,7 @@ private function createTemporaryTable() self::FIELD_SCORE, Table::TYPE_DECIMAL, [32, 16], - ['unsigned' => true, 'nullable' => false], + ['unsigned' => true, 'nullable' => true], 'Score' ); $table->setOption('type', 'memory'); From cbe25f4960b59feed344eebee25e3dbe2f3efaaa Mon Sep 17 00:00:00 2001 From: Pieter Hoste Date: Sat, 7 Sep 2019 14:51:53 +0200 Subject: [PATCH 083/147] Cleaned up non-existing files from being excluded from bundling. --- app/design/adminhtml/Magento/backend/etc/view.xml | 9 --------- app/design/frontend/Magento/blank/etc/view.xml | 2 -- app/design/frontend/Magento/luma/etc/view.xml | 2 -- 3 files changed, 13 deletions(-) diff --git a/app/design/adminhtml/Magento/backend/etc/view.xml b/app/design/adminhtml/Magento/backend/etc/view.xml index 18c2d8f1b1722..6539e3330e98b 100644 --- a/app/design/adminhtml/Magento/backend/etc/view.xml +++ b/app/design/adminhtml/Magento/backend/etc/view.xml @@ -24,7 +24,6 @@ Lib::mage/captcha.js - Lib::mage/captcha.min.js Lib::mage/common.js Lib::mage/cookies.js Lib::mage/dataPost.js @@ -46,7 +45,6 @@ Lib::mage/translate-inline-vde.js Lib::mage/webapi.js Lib::mage/zoom.js - Lib::mage/validation/dob-rule.js Lib::mage/validation/validation.js Lib::mage/adminhtml/varienLoader.js Lib::mage/adminhtml/tools.js @@ -57,11 +55,9 @@ Lib::jquery/jquery.parsequery.js Lib::jquery/jquery.mobile.custom.js Lib::jquery/jquery-ui.js - Lib::jquery/jquery-ui.min.js Lib::matchMedia.js Lib::requirejs/require.js Lib::requirejs/text.js - Lib::date-format-normalizer.js Lib::varien/js.js Lib::css Lib::lib @@ -72,10 +68,5 @@ Lib::fotorama Lib::magnifier Lib::tiny_mce - Lib::tiny_mce/classes - Lib::tiny_mce/langs - Lib::tiny_mce/plugins - Lib::tiny_mce/themes - Lib::tiny_mce/utils diff --git a/app/design/frontend/Magento/blank/etc/view.xml b/app/design/frontend/Magento/blank/etc/view.xml index e742ce0a21cd1..dfc5ad39af557 100644 --- a/app/design/frontend/Magento/blank/etc/view.xml +++ b/app/design/frontend/Magento/blank/etc/view.xml @@ -262,12 +262,10 @@ Lib::jquery/jquery.min.js Lib::jquery/jquery-ui-1.9.2.js Lib::jquery/jquery.details.js - Lib::jquery/jquery.details.min.js Lib::jquery/jquery.hoverIntent.js Lib::jquery/colorpicker/js/colorpicker.js Lib::requirejs/require.js Lib::requirejs/text.js - Lib::date-format-normalizer.js Lib::legacy-build.min.js Lib::mage/captcha.js Lib::mage/dropdown_old.js diff --git a/app/design/frontend/Magento/luma/etc/view.xml b/app/design/frontend/Magento/luma/etc/view.xml index 7aa2e51481bd9..c04c0d73cb3cd 100644 --- a/app/design/frontend/Magento/luma/etc/view.xml +++ b/app/design/frontend/Magento/luma/etc/view.xml @@ -273,12 +273,10 @@ Lib::jquery/jquery.min.js Lib::jquery/jquery-ui-1.9.2.js Lib::jquery/jquery.details.js - Lib::jquery/jquery.details.min.js Lib::jquery/jquery.hoverIntent.js Lib::jquery/colorpicker/js/colorpicker.js Lib::requirejs/require.js Lib::requirejs/text.js - Lib::date-format-normalizer.js Lib::legacy-build.min.js Lib::mage/captcha.js Lib::mage/dropdown_old.js From 09ef8d2a905c52dc057b8e6776b1a2e9dc529e65 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Mon, 9 Sep 2019 12:59:46 -0500 Subject: [PATCH 084/147] MC-18450: Allow custom attributes to be filterable and also returned in the layered navigation the product filter section in GraphQL - fix tests affected by making url_key searchable - fix unit and integration tests --- .../Adapter/Mysql/Filter/Preprocessor.php | 4 +- .../Product/FieldProvider/StaticFieldTest.php | 94 ++++++++++++------- .../Indexer/Fulltext/Action/FullTest.php | 18 +++- .../CatalogSearch/_files/indexer_fulltext.php | 5 + .../Controller/GraphQlControllerTest.php | 2 +- .../Adapter/Mysql/TemporaryStorageTest.php | 6 +- 6 files changed, 83 insertions(+), 46 deletions(-) diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php index f164da99292ad..a97d362c5de7f 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php @@ -201,8 +201,8 @@ private function processQueryWithField(FilterInterface $filter, $isNegation, $qu ) ->joinLeft( ['current_store' => $table], - 'current_store.entity_id = main_table.entity_id AND ' - . 'current_store.attribute_id = main_table.attribute_id AND current_store.store_id = ' + "current_store.{$linkIdField} = main_table.{$linkIdField} AND " + . "current_store.attribute_id = main_table.attribute_id AND current_store.store_id = " . $currentStoreId, null ) diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php index de85b8b6602b8..f90c13c9bfb65 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php @@ -139,6 +139,7 @@ public function testGetAllAttributesTypes( $isComplexType, $complexType, $isSortable, + $isTextType, $fieldName, $compositeFieldName, $sortFieldName, @@ -153,29 +154,33 @@ public function testGetAllAttributesTypes( $this->indexTypeConverter->expects($this->any()) ->method('convert') ->with($this->anything()) - ->will($this->returnCallback( - function ($type) { - if ($type === 'no_index') { - return 'no'; - } elseif ($type === 'no_analyze') { - return 'not_analyzed'; + ->will( + $this->returnCallback( + function ($type) { + if ($type === 'no_index') { + return 'no'; + } elseif ($type === 'no_analyze') { + return 'not_analyzed'; + } } - } - )); + ) + ); $this->fieldNameResolver->expects($this->any()) ->method('getFieldName') ->with($this->anything()) - ->will($this->returnCallback( - function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortFieldName) { - if (empty($context)) { - return $fieldName; - } elseif ($context['type'] === 'sort') { - return $sortFieldName; - } elseif ($context['type'] === 'text') { - return $compositeFieldName; + ->will( + $this->returnCallback( + function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortFieldName) { + if (empty($context)) { + return $fieldName; + } elseif ($context['type'] === 'sort') { + return $sortFieldName; + } elseif ($context['type'] === 'text') { + return $compositeFieldName; + } } - } - )); + ) + ); $productAttributeMock = $this->getMockBuilder(AbstractAttribute::class) ->setMethods(['getAttributeCode']) @@ -189,7 +194,7 @@ function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortF $attributeMock = $this->getMockBuilder(AttributeAdapter::class) ->disableOriginalConstructor() - ->setMethods(['isComplexType', 'getAttributeCode', 'isSortable']) + ->setMethods(['isComplexType', 'getAttributeCode', 'isSortable', 'isTextType']) ->getMock(); $attributeMock->expects($this->any()) ->method('isComplexType') @@ -197,6 +202,9 @@ function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortF $attributeMock->expects($this->any()) ->method('isSortable') ->willReturn($isSortable); + $attributeMock->expects($this->any()) + ->method('isTextType') + ->willReturn($isTextType); $attributeMock->expects($this->any()) ->method('getAttributeCode') ->willReturn($attributeCode); @@ -207,22 +215,24 @@ function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortF $this->fieldTypeConverter->expects($this->any()) ->method('convert') ->with($this->anything()) - ->will($this->returnCallback( - function ($type) use ($complexType) { - static $callCount = []; - $callCount[$type] = !isset($callCount[$type]) ? 1 : ++$callCount[$type]; + ->will( + $this->returnCallback( + function ($type) use ($complexType) { + static $callCount = []; + $callCount[$type] = !isset($callCount[$type]) ? 1 : ++$callCount[$type]; - if ($type === 'string') { - return 'string'; - } elseif ($type === 'float') { - return 'float'; - } elseif ($type === 'keyword') { - return 'string'; - } else { - return $complexType; + if ($type === 'string') { + return 'string'; + } elseif ($type === 'float') { + return 'float'; + } elseif ($type === 'keyword') { + return 'string'; + } else { + return $complexType; + } } - } - )); + ) + ); $this->assertEquals( $expected, @@ -243,13 +253,19 @@ public function attributeProvider() true, 'text', false, + true, 'category_ids', 'category_ids_value', '', [ 'category_ids' => [ 'type' => 'select', - 'index' => true + 'index' => true, + 'fields' => [ + 'keyword' => [ + 'type' => 'string', + ] + ] ], 'category_ids_value' => [ 'type' => 'string' @@ -267,13 +283,19 @@ public function attributeProvider() false, null, false, + true, 'attr_code', '', '', [ 'attr_code' => [ 'type' => 'text', - 'index' => 'no' + 'index' => 'no', + 'fields' => [ + 'keyword' => [ + 'type' => 'string', + ] + ] ], 'store_id' => [ 'type' => 'string', @@ -288,6 +310,7 @@ public function attributeProvider() false, null, false, + false, 'attr_code', '', '', @@ -308,6 +331,7 @@ public function attributeProvider() false, null, true, + false, 'attr_code', '', 'sort_attr_code', diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php index 137a3845b1efa..916af235edbd8 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php @@ -82,37 +82,45 @@ private function getExpectedIndexData() $taxClassId = $attributeRepository ->get(\Magento\Customer\Api\Data\GroupInterface::TAX_CLASS_ID) ->getAttributeId(); + $urlKeyId = $attributeRepository + ->get(\Magento\Catalog\Api\Data\ProductAttributeInterface::CODE_SEO_FIELD_URL_KEY) + ->getAttributeId(); return [ 'configurable' => [ $skuId => 'configurable', $configurableId => 'Option 2', $nameId => 'Configurable Product | Configurable OptionOption 2', $taxClassId => 'Taxable Goods | Taxable Goods', - $statusId => 'Enabled | Enabled' + $statusId => 'Enabled | Enabled', + $urlKeyId => 'configurable-product | configurable-optionoption-2' ], 'index_enabled' => [ $skuId => 'index_enabled', $nameId => 'index enabled', $taxClassId => 'Taxable Goods', - $statusId => 'Enabled' + $statusId => 'Enabled', + $urlKeyId => 'index-enabled' ], 'index_visible_search' => [ $skuId => 'index_visible_search', $nameId => 'index visible search', $taxClassId => 'Taxable Goods', - $statusId => 'Enabled' + $statusId => 'Enabled', + $urlKeyId => 'index-visible-search' ], 'index_visible_category' => [ $skuId => 'index_visible_category', $nameId => 'index visible category', $taxClassId => 'Taxable Goods', - $statusId => 'Enabled' + $statusId => 'Enabled', + $urlKeyId => 'index-visible-category' ], 'index_visible_both' => [ $skuId => 'index_visible_both', $nameId => 'index visible both', $taxClassId => 'Taxable Goods', - $statusId => 'Enabled' + $statusId => 'Enabled', + $urlKeyId => 'index-visible-both' ] ]; } diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/indexer_fulltext.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/indexer_fulltext.php index 2ed7c1a45360d..0e5987f8326a5 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/indexer_fulltext.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/indexer_fulltext.php @@ -14,6 +14,7 @@ ->setWebsiteIds([1]) ->setName('Simple Product Apple') ->setSku('fulltext-1') + ->setUrlKey('fulltext-1') ->setPrice(10) ->setMetaTitle('first meta title') ->setMetaKeyword('first meta keyword') @@ -30,6 +31,7 @@ ->setWebsiteIds([1]) ->setName('Simple Product Banana') ->setSku('fulltext-2') + ->setUrlKey('fulltext-2') ->setPrice(20) ->setMetaTitle('second meta title') ->setMetaKeyword('second meta keyword') @@ -46,6 +48,7 @@ ->setWebsiteIds([1]) ->setName('Simple Product Orange') ->setSku('fulltext-3') + ->setUrlKey('fulltext-3') ->setPrice(20) ->setMetaTitle('third meta title') ->setMetaKeyword('third meta keyword') @@ -62,6 +65,7 @@ ->setWebsiteIds([1]) ->setName('Simple Product Papaya') ->setSku('fulltext-4') + ->setUrlKey('fulltext-4') ->setPrice(20) ->setMetaTitle('fourth meta title') ->setMetaKeyword('fourth meta keyword') @@ -78,6 +82,7 @@ ->setWebsiteIds([1]) ->setName('Simple Product Cherry') ->setSku('fulltext-5') + ->setUrlKey('fulltext-5') ->setPrice(20) ->setMetaTitle('fifth meta title') ->setMetaKeyword('fifth meta keyword') diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php b/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php index d0d746812ec44..b47cea971811e 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php @@ -167,7 +167,7 @@ public function testDispatchGetWithParameterizedVariables() : void /** @var ProductInterface $product */ $product = $productRepository->get('simple1'); $query = <<adapter->expects($this->once()) ->method('dropTemporaryTable'); - $tableInteractionCount += 1; + $tableInteractionCount++; } $table->expects($this->at($tableInteractionCount)) ->method('addColumn') @@ -187,14 +187,14 @@ private function createTemporaryTable($persistentConnection = true) ['unsigned' => true, 'nullable' => false, 'primary' => true], 'Entity ID' ); - $tableInteractionCount += 1; + $tableInteractionCount++; $table->expects($this->at($tableInteractionCount)) ->method('addColumn') ->with( 'score', Table::TYPE_DECIMAL, [32, 16], - ['unsigned' => true, 'nullable' => false], + ['unsigned' => true, 'nullable' => true], 'Score' ); $table->expects($this->once()) From e3ecfb8e29ed74421075c3099d95ac286a091e02 Mon Sep 17 00:00:00 2001 From: Raoul Rego Date: Mon, 9 Sep 2019 13:30:34 -0500 Subject: [PATCH 085/147] MC-17627: Dependency static test does not analyze content of phtml files - Fixed codestyle --- .../Magento/TestFramework/Dependency/PhpRule.php | 6 +++--- .../testsuite/Magento/Test/Integrity/DependencyTest.php | 8 +++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index 33f213454aaa4..913cc9448b978 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -290,7 +290,7 @@ private function isPluginDependency($dependent, $dependency) protected function _caseGetUrl(string $currentModule, string &$contents): array { $pattern = '#(\->|:)(?getUrl\(([\'"])(?[a-z0-9\-_]{3,}|\*)' - .'(\/(?[a-z0-9\-_]+|\*))?(\/(?[a-z0-9\-_]+|\*))?\3)#i'; + .'(/(?[a-z0-9\-_]+|\*))?(/(?[a-z0-9\-_]+|\*))?\3)#i'; $dependencies = []; if (!preg_match_all($pattern, $contents, $matches, PREG_SET_ORDER)) { @@ -304,11 +304,11 @@ protected function _caseGetUrl(string $currentModule, string &$contents): array $actionName = $item['action_name'] ?? UrlInterface::DEFAULT_ACTION_NAME; // skip rest - if ($routeId === "rest") { //MC-17627 + if ($routeId === "rest") { //MC-19890 continue; } // skip wildcards - if ($routeId === "*" || $controllerName === "*" || $actionName === "*") { //MC-17627 + if ($routeId === "*" || $controllerName === "*" || $actionName === "*") { //MC-19890 continue; } $modules = $this->routeMapper->getDependencyByRoutePath( diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php index 144386df55e3a..a644f8894d08f 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php @@ -265,14 +265,12 @@ private static function getRoutesWhitelist(): array { if (is_null(self::$routesWhitelist)) { $routesWhitelistFilePattern = realpath(__DIR__) . '/_files/dependency_test/whitelist/routes_*.php'; - self::$routesWhitelist = []; + $routesWhitelist = []; foreach (glob($routesWhitelistFilePattern) as $fileName) { //phpcs:ignore Magento2.Performance.ForeachArrayMerge - self::$routesWhitelist = array_merge( - self::$routesWhitelist, - include $fileName - ); + $routesWhitelist = array_merge($routesWhitelist, include $fileName); } + self::$routesWhitelist = $routesWhitelist; } return self::$routesWhitelist; } From 7c3106a5c8f47d64fb5d18cbc7365e1fc93cf29b Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Mon, 9 Sep 2019 13:50:16 -0500 Subject: [PATCH 086/147] MC-18450: Allow custom attributes to be filterable and also returned in the layered navigation the product filter section in GraphQL - Sort by position if only filtering --- .../Product/SearchCriteriaBuilder.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php index 810e9172d0973..0e92bbbab4259 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -7,6 +7,7 @@ namespace Magento\CatalogGraphQl\DataProvider\Product; +use Magento\Catalog\Api\Data\EavAttributeInterface; use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\Search\FilterGroupBuilder; use Magento\Framework\Api\Search\SearchCriteriaInterface; @@ -84,6 +85,7 @@ public function __construct( public function build(array $args, bool $includeAggregation): SearchCriteriaInterface { $searchCriteria = $this->builder->build('products', $args); + $isSearch = !empty($args['search']); $this->updateRangeFilters($searchCriteria); if ($includeAggregation) { @@ -94,13 +96,15 @@ public function build(array $args, bool $includeAggregation): SearchCriteriaInte } $searchCriteria->setRequestName($requestName); - if (!empty($args['search'])) { + if ($isSearch) { $this->addFilter($searchCriteria, 'search_term', $args['search']); } + if (!$searchCriteria->getSortOrders()) { - $this->addDefaultSortOrder($searchCriteria); + $this->addDefaultSortOrder($searchCriteria, $isSearch); } - $this->addVisibilityFilter($searchCriteria, !empty($args['search']), !empty($args['filter'])); + + $this->addVisibilityFilter($searchCriteria, $isSearch, !empty($args['filter'])); $searchCriteria->setCurrentPage($args['currentPage']); $searchCriteria->setPageSize($args['pageSize']); @@ -168,12 +172,15 @@ private function addFilter(SearchCriteriaInterface $searchCriteria, string $fiel * Sort by relevance DESC by default * * @param SearchCriteriaInterface $searchCriteria + * @param bool $isSearch */ - private function addDefaultSortOrder(SearchCriteriaInterface $searchCriteria): void + private function addDefaultSortOrder(SearchCriteriaInterface $searchCriteria, $isSearch = false): void { + $sortField = $isSearch ? 'relevance' : EavAttributeInterface::POSITION; + $sortDirection = $isSearch ? SortOrder::SORT_DESC : SortOrder::SORT_ASC; $defaultSortOrder = $this->sortOrderBuilder - ->setField('relevance') - ->setDirection(SortOrder::SORT_DESC) + ->setField($sortField) + ->setDirection($sortDirection) ->create(); $searchCriteria->setSortOrders([$defaultSortOrder]); From a2f41c7455588d631f20e1cb7ffec879312abfd1 Mon Sep 17 00:00:00 2001 From: Oleksandr Dubovyk Date: Mon, 9 Sep 2019 14:44:12 -0500 Subject: [PATCH 087/147] MC-17948: Newsletter template preview show small section with scroll when image added --- .../adminhtml/Magento/backend/web/css/styles-old.less | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/design/adminhtml/Magento/backend/web/css/styles-old.less b/app/design/adminhtml/Magento/backend/web/css/styles-old.less index 53af8933343f1..44fca79c31be5 100644 --- a/app/design/adminhtml/Magento/backend/web/css/styles-old.less +++ b/app/design/adminhtml/Magento/backend/web/css/styles-old.less @@ -4060,6 +4060,16 @@ } } +.newsletter-template-preview { + height: 100%; + .cms-revision-preview { + height: 100%; + .preview_iframe { + height: calc(~'100% - 50px'); + } + } +} + .admin__scope-old { .buttons-set { margin: 0 0 15px; From 9c413b9c746cb99044d3a57dd65ebb81255a67dd Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Mon, 9 Sep 2019 14:46:32 -0500 Subject: [PATCH 088/147] MC-18450: Allow custom attributes to be filterable and also returned in the layered navigation the product filter section in GraphQL --- .../CatalogGraphQl/Model/Resolver/Products/Query/Search.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index a6d7f8641bd86..ef83cc6132ecc 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -110,6 +110,8 @@ public function getResult( } else { $maxPages = 0; } + $searchCriteria->setPageSize($realPageSize); + $searchCriteria->setCurrentPage($realCurrentPage); $productArray = []; /** @var \Magento\Catalog\Model\Product $product */ From c9ed703b2d2a650c18d4e906e02c1b7be6013ee8 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Mon, 9 Sep 2019 14:57:11 -0500 Subject: [PATCH 089/147] MC-18450: Allow custom attributes to be filterable and also returned in the layered navigation the product filter section in GraphQL - Update performance tests to use url keys --- setup/performance-toolkit/benchmark.jmx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/setup/performance-toolkit/benchmark.jmx b/setup/performance-toolkit/benchmark.jmx index 8ffec6aff0422..a4128576e8e15 100644 --- a/setup/performance-toolkit/benchmark.jmx +++ b/setup/performance-toolkit/benchmark.jmx @@ -39784,7 +39784,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetail($name: String, $onServer: Boolean!) {\n productDetail: products(filter: { name: { eq: $name } }) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetail"} + {"query":"query productDetail($url_key: String, $onServer: Boolean!) {\n productDetail: products(filter: { url_key: { eq: $url_key } }) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"url_key":"${product_url_key}","onServer":false},"operationName":"productDetail"} = @@ -40032,7 +40032,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetailByName($name: String, $onServer: Boolean!) {\n products(filter: { name: { eq: $name } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetailByName"} + {"query":"query productDetailByName($url_key: String, $onServer: Boolean!) {\n products(filter: { url_key: { eq: $url_key } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"url_key":"${product_url_key}","onServer":false},"operationName":"productDetailByName"} = @@ -41674,7 +41674,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetailByName($name: String, $onServer: Boolean!) {\n products(filter: { name: { eq: $name } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetailByName"} + {"query":"query productDetailByName($url_key: String, $onServer: Boolean!) {\n products(filter: { url_key: { eq: $url_key } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"url_key":"${product_url_key}","onServer":false},"operationName":"productDetailByName"} = @@ -42152,7 +42152,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetailByName($name: String, $onServer: Boolean!) {\n products(filter: { name: { eq: $name } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetailByName"} + {"query":"query productDetailByName($url_key: String, $onServer: Boolean!) {\n products(filter: { url_key: { eq: $url_key } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"url_key":"${product_url_key}","onServer":false},"operationName":"productDetailByName"} = @@ -42716,7 +42716,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetailByName($name: String, $onServer: Boolean!) {\n products(filter: { name: { eq: $name } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetailByName"} + {"query":"query productDetailByName($url_key: String, $onServer: Boolean!) {\n products(filter: { url_key: { eq: $url_key } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"url_key":"${product_url_key}","onServer":false},"operationName":"productDetailByName"} = @@ -43664,7 +43664,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetailByName($name: String, $onServer: Boolean!) {\n products(filter: { name: { eq: $name } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetailByName"} + {"query":"query productDetailByName($url_key: String, $onServer: Boolean!) {\n products(filter: { url_key: { eq: $url_key } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"url_key":"${product_url_key}","onServer":false},"operationName":"productDetailByName"} = @@ -43762,7 +43762,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetail($name: String, $onServer: Boolean!) {\n productDetail: products(filter: { name: { eq: $name } }) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetail"} + {"query":"query productDetail($url_key: String, $onServer: Boolean!) {\n productDetail: products(filter: { url_key: { eq: $url_key } }) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"url_key":"${product_url_key}","onServer":false},"operationName":"productDetail"} = @@ -44066,7 +44066,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetailByName($name: String, $onServer: Boolean!) {\n products(filter: { name: { eq: $name } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetailByName"} + {"query":"query productDetailByName($url_key: String, $onServer: Boolean!) {\n products(filter: { url_key: { eq: $url_key } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"url_key":"${product_url_key}","onServer":false},"operationName":"productDetailByName"} = From 92adff00b6062eae519b59b761416319114f4618 Mon Sep 17 00:00:00 2001 From: Raoul Rego Date: Mon, 9 Sep 2019 15:17:27 -0500 Subject: [PATCH 090/147] MC-17627: Dependency static test does not analyze content of phtml files - Fixed codestyle --- .../static/testsuite/Magento/Test/Integrity/DependencyTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php index a644f8894d08f..fa0d365061858 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php @@ -235,7 +235,7 @@ protected static function _initRules() $dbRuleTables = []; foreach (glob($replaceFilePattern) as $fileName) { //phpcs:ignore Magento2.Performance.ForeachArrayMerge - $dbRuleTables = array_merge($dbRuleTables, @include $fileName); + $dbRuleTables = array_merge($dbRuleTables, include $fileName); } self::$_rulesInstances = [ new PhpRule( From 0d49eeb8e8f85dca5b0ebfb8e884b988f8fcbd9f Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi Date: Mon, 9 Sep 2019 15:43:15 -0500 Subject: [PATCH 091/147] MC-19917: 2 Place order buttons appear on the PayPal Review page with PayPal Express only --- .../Paypal/view/frontend/templates/express/review.phtml | 4 ---- app/code/Magento/Paypal/view/frontend/web/js/order-review.js | 4 +--- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/app/code/Magento/Paypal/view/frontend/templates/express/review.phtml b/app/code/Magento/Paypal/view/frontend/templates/express/review.phtml index 7a94ac56232bc..8e222ca7eb04d 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/express/review.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/express/review.phtml @@ -136,10 +136,6 @@ value="escapeHtml(__('Place Order')) ?>"> escapeHtml(__('Place Order')) ?> -