From 2e4beb00e905867fd6a1f82ffd8930bd1b6b71d9 Mon Sep 17 00:00:00 2001 From: Andrii Voskoboinikov Date: Wed, 18 Apr 2018 15:26:01 +0300 Subject: [PATCH 1/3] MAGETWO-90564: [Indexer optimizations] Catalog Rule indexer matching mechanism optimization --- .../EavAttributeCondition.php | 129 ++ .../ConditionBuilder/Factory.php | 84 ++ .../NativeAttributeCondition.php | 97 ++ .../ConditionProcessor/DefaultCondition.php | 43 + .../ProductCategoryCondition.php | 124 ++ .../ConditionBuilder/FactoryTest.php | 139 +++ app/code/Magento/Catalog/etc/di.xml | 6 + .../Product/ConditionsToCollectionApplier.php | 78 ++ app/code/Magento/CatalogRule/Model/Rule.php | 55 +- .../ConditionsToSearchCriteriaMapper.php | 308 +++++ .../Condition/MappableConditionsProcessor.php | 139 +++ .../MappableConditionProcessorTest.php | 1035 +++++++++++++++++ app/code/Magento/CatalogRule/etc/di.xml | 23 + .../ConditionsToCollectionApplierTest.php | 1015 ++++++++++++++++ .../attribute_sets.php | 50 + .../attribute_sets_rollback.php | 25 + .../conditions_to_collection/categories.php | 83 ++ .../categories_rollback.php | 28 + .../conditions_to_collection/products.php | 206 ++++ .../products_rollback.php | 23 + .../Framework/Api/CombinedFilterGroup.php | 78 ++ .../AdvancedFilterProcessor.php | 131 +++ .../CustomConditionInterface.php | 34 + .../CustomConditionProvider.php | 69 ++ .../CustomConditionProviderInterface.php | 37 + .../CustomConditionProviderTest.php | 91 ++ 26 files changed, 4121 insertions(+), 9 deletions(-) create mode 100644 app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php create mode 100644 app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/Factory.php create mode 100644 app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php create mode 100644 app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/DefaultCondition.php create mode 100644 app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php create mode 100644 app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/FactoryTest.php create mode 100644 app/code/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplier.php create mode 100644 app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php create mode 100644 app/code/Magento/CatalogRule/Model/Rule/Condition/MappableConditionsProcessor.php create mode 100644 app/code/Magento/CatalogRule/Test/Unit/Model/Rule/Condition/MappableConditionProcessorTest.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplierTest.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/attribute_sets.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/attribute_sets_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/categories.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/categories_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products_rollback.php create mode 100644 lib/internal/Magento/Framework/Api/CombinedFilterGroup.php create mode 100644 lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/AdvancedFilterProcessor.php create mode 100644 lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/CustomConditionInterface.php create mode 100644 lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/CustomConditionProvider.php create mode 100644 lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/CustomConditionProviderInterface.php create mode 100644 lib/internal/Magento/Framework/Api/Test/Unit/SearchCriteria/CollectionProcessor/ConditionProcessor/CustomConditionProviderTest.php diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php new file mode 100644 index 0000000000000..ca8b53ea0db00 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php @@ -0,0 +1,129 @@ +eavConfig = $eavConfig; + $this->resourceConnection = $resourceConnection; + } + + /** + * @param Filter $filter + * @return string + * @throws \DomainException + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function build(Filter $filter): string + { + $attribute = $this->getAttributeByCode($filter->getField()); + $tableAlias = 'ca_' . $attribute->getAttributeCode(); + + $conditionType = $this->mapConditionType($filter->getConditionType()); + $conditionValue = $this->mapConditionValue($conditionType, $filter->getValue()); + + // NOTE: store scope was ignored intentionally to perform search across all stores + $attributeSelect = $this->resourceConnection->getConnection() + ->select() + ->from( + [$tableAlias => $attribute->getBackendTable()], + $tableAlias . '.' . $attribute->getEntityIdField() + )->where( + $this->resourceConnection->getConnection()->prepareSqlCondition( + $tableAlias . '.' . $attribute->getIdFieldName(), + ['eq' => $attribute->getAttributeId()] + ) + )->where( + $this->resourceConnection->getConnection()->prepareSqlCondition( + $tableAlias . '.value', + [$conditionType => $conditionValue] + ) + ); + + return $this->resourceConnection + ->getConnection() + ->prepareSqlCondition( + Collection::MAIN_TABLE_ALIAS . '.' . $attribute->getEntityIdField(), + [ + 'in' => $attributeSelect + ] + ); + } + + /** + * @param string $field + * @return Attribute + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getAttributeByCode(string $field): Attribute + { + return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $field); + } + + /** + * Map equal and not equal conditions to in and not in + * + * @param string $conditionType + * @return mixed + */ + private function mapConditionType(string $conditionType): string + { + $conditionsMap = [ + 'eq' => 'in', + 'neq' => 'nin' + ]; + + return isset($conditionsMap[$conditionType]) ? $conditionsMap[$conditionType] : $conditionType; + } + + /** + * Wraps value with '%' if condition type is 'like' or 'not like' + * + * @param string $conditionType + * @param string $conditionValue + * @return string + */ + private function mapConditionValue(string $conditionType, string $conditionValue): string + { + $conditionsMap = ['like', 'nlike']; + + if (in_array($conditionType, $conditionsMap)) { + $conditionValue = '%' . $conditionValue . '%'; + } + + return $conditionValue; + } +} diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/Factory.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/Factory.php new file mode 100644 index 0000000000000..808878d1481a9 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/Factory.php @@ -0,0 +1,84 @@ +eavConfig = $eavConfig; + $this->productResource = $productResource; + $this->eavAttributeConditionBuilder = $eavAttributeConditionBuilder; + $this->nativeAttributeConditionBuilder = $nativeAttributeConditionBuilder; + } + + /** + * @param Filter $filter + * @return CustomConditionInterface + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function createByFilter(Filter $filter): CustomConditionInterface + { + $attribute = $this->getAttributeByCode($filter->getField()); + + if ($attribute->getBackendTable() === $this->productResource->getEntityTable()) { + return $this->nativeAttributeConditionBuilder; + } + + return $this->eavAttributeConditionBuilder; + } + + /** + * @param string $field + * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getAttributeByCode(string $field): Attribute + { + return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $field); + } +} diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php new file mode 100644 index 0000000000000..976714dcd9aea --- /dev/null +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php @@ -0,0 +1,97 @@ +resourceConnection = $resourceConnection; + } + + /** + * @param Filter $filter + * @return string + * @throws \DomainException + */ + public function build(Filter $filter): string + { + $conditionType = $this->mapConditionType($filter->getConditionType(), $filter->getField()); + $conditionValue = $this->mapConditionValue($conditionType, $filter->getValue()); + + return $this->resourceConnection + ->getConnection() + ->prepareSqlCondition( + Collection::MAIN_TABLE_ALIAS . '.' . $filter->getField(), + [ + $conditionType => $conditionValue + ] + ); + } + + /** + * Map equal and not equal conditions to in and not in + * + * @param string $conditionType + * @param string $field + * @return mixed + */ + private function mapConditionType(string $conditionType, string $field): string + { + if (strtolower($field) === ProductInterface::SKU) { + $conditionsMap = [ + 'eq' => 'like', + 'neq' => 'nlike' + ]; + } else { + $conditionsMap = [ + 'eq' => 'in', + 'neq' => 'nin' + ]; + } + + return isset($conditionsMap[$conditionType]) ? $conditionsMap[$conditionType] : $conditionType; + } + + /** + * Wraps value with '%' if condition type is 'like' or 'not like' + * + * @param string $conditionType + * @param string $conditionValue + * @return string + */ + private function mapConditionValue(string $conditionType, string $conditionValue): string + { + $conditionsMap = ['like', 'nlike']; + + if (in_array($conditionType, $conditionsMap)) { + $conditionValue = '%' . $conditionValue . '%'; + } + + return $conditionValue; + } +} diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/DefaultCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/DefaultCondition.php new file mode 100644 index 0000000000000..5189da35ab52a --- /dev/null +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/DefaultCondition.php @@ -0,0 +1,43 @@ +conditionBuilderFactory = $conditionBuilderFactory; + } + + /** + * @param Filter $filter + * @return string + */ + public function build(Filter $filter): string + { + $filterBuilder = $this->conditionBuilderFactory->createByFilter($filter); + + return $filterBuilder->build($filter); + } +} diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php new file mode 100644 index 0000000000000..09c3a3864bbc7 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php @@ -0,0 +1,124 @@ +resourceConnection = $resourceConnection; + $this->categoryRepository = $categoryRepository; + } + + /** + * @param Filter $filter + * @return string + */ + public function build(Filter $filter): string + { + $categorySelect = $this->resourceConnection->getConnection()->select() + ->from( + ['cat' => $this->resourceConnection->getTableName('catalog_category_product')], + 'cat.product_id' + )->where( + $this->resourceConnection->getConnection()->prepareSqlCondition( + 'cat.category_id', + [$this->mapConditionType($filter->getConditionType()) => $this->getCategoryIds($filter)] + ) + ); + + $selectCondition = [ + 'in' => $categorySelect + ]; + + return $this->resourceConnection->getConnection() + ->prepareSqlCondition(Collection::MAIN_TABLE_ALIAS . '.entity_id', $selectCondition); + } + + /** + * Extracts required category ids from Filter + * If category is anchor all children categories will be included too + * If category is root all children categories will be included too + * + * @param Filter $filter + * @return array + */ + private function getCategoryIds(Filter $filter): array + { + $categoryIds = explode(',', $filter->getValue()); + $childCategoryIds = []; + + foreach ($categoryIds as $categoryId) { + try { + $category = $this->categoryRepository->get($categoryId); + } catch (CategoryDoesNotExistException $exception) { + continue; + } + + if ($category->getIsAnchor()) { + $childCategoryIds[] = $category->getAllChildren(true); + } + + // This is the simplest way to check if category is root + if ((int)$category->getLevel() === $this->rootCategoryLevel) { + $childCategoryIds[] = $category->getAllChildren(true); + } + } + + return array_unique(array_merge($categoryIds, ...$childCategoryIds)); + } + + /** + * Map equal and not equal conditions to in and not in + * + * @param string $conditionType + * @return mixed + */ + private function mapConditionType(string $conditionType): string + { + $conditionsMap = [ + 'eq' => 'in', + 'neq' => 'nin', + 'like' => 'in', + 'nlike' => 'nin', + ]; + return $conditionsMap[$conditionType] ?? $conditionType; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/FactoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/FactoryTest.php new file mode 100644 index 0000000000000..70b75e7ff2790 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/FactoryTest.php @@ -0,0 +1,139 @@ +productResourceMock = $this->getMockBuilder(ProductResource::class) + ->disableOriginalConstructor() + ->setMethods(['getEntityTable']) + ->getMock(); + + $this->eavConfigMock = $this->getMockBuilder(EavConfig::class) + ->disableOriginalConstructor() + ->setMethods(['getAttribute']) + ->getMock(); + + $this->eavAttrConditionBuilderMock = $this->getMockBuilder(CustomConditionInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->nativeAttrConditionBuilderMock = $this->getMockBuilder(CustomConditionInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->conditionBuilderFactory = $objectManagerHelper->getObject( + Factory::class, + [ + 'eavConfig' => $this->eavConfigMock, + 'productResource' => $this->productResourceMock, + 'eavAttributeConditionBuilder' => $this->eavAttrConditionBuilderMock, + 'nativeAttributeConditionBuilder' => $this->nativeAttrConditionBuilderMock, + ] + ); + } + + public function testNativeAttrConditionBuilder() + { + $fieldName = 'super_field'; + $attributeTable = 'my-table'; + $productResourceTable = 'my-table'; + + $filterMock = $this->getMockBuilder(Filter::class) + ->disableOriginalConstructor() + ->setMethods(['getField']) + ->getMock(); + + $filterMock + ->method('getField') + ->willReturn($fieldName); + + $attributeMock = $this->getMockBuilder(Attribute::class) + ->disableOriginalConstructor() + ->setMethods(['getBackendTable']) + ->getMock(); + + $this->eavConfigMock + ->method('getAttribute') + ->with(Product::ENTITY, $fieldName) + ->willReturn($attributeMock); + + $attributeMock + ->method('getBackendTable') + ->willReturn($attributeTable); + + $this->productResourceMock + ->method('getEntityTable') + ->willReturn($productResourceTable); + + $this->assertEquals( + $this->nativeAttrConditionBuilderMock, + $this->conditionBuilderFactory->createByFilter($filterMock) + ); + } + + public function testEavAttrConditionBuilder() + { + $fieldName = 'super_field'; + $attributeTable = 'my-table'; + $productResourceTable = 'not-my-table'; + + $filterMock = $this->getMockBuilder(Filter::class) + ->disableOriginalConstructor() + ->setMethods(['getField']) + ->getMock(); + + $filterMock + ->method('getField') + ->willReturn($fieldName); + + $attributeMock = $this->getMockBuilder(Attribute::class) + ->disableOriginalConstructor() + ->setMethods(['getBackendTable']) + ->getMock(); + + $this->eavConfigMock + ->method('getAttribute') + ->with(Product::ENTITY, $fieldName) + ->willReturn($attributeMock); + + $attributeMock + ->method('getBackendTable') + ->willReturn($attributeTable); + + $this->productResourceMock + ->method('getEntityTable') + ->willReturn($productResourceTable); + + $this->assertEquals( + $this->eavAttrConditionBuilderMock, + $this->conditionBuilderFactory->createByFilter($filterMock) + ); + } +} diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index a3cd2d8558f87..84b416e14316c 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -1078,4 +1078,10 @@ indexer + + + Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\ConditionBuilder\EavAttributeCondition + Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\ConditionBuilder\NativeAttributeCondition + + diff --git a/app/code/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplier.php b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplier.php new file mode 100644 index 0000000000000..13809a381ecb9 --- /dev/null +++ b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplier.php @@ -0,0 +1,78 @@ +conditionsToSearchCriteriaMapper = $conditionsToSearchCriteriaMapper; + $this->searchCriteriaProcessor = $searchCriteriaProcessor; + $this->mappableConditionsProcessor = $mappableConditionsProcessor; + } + + /** + * Transforms catalog rule conditions to search criteria + * and applies them on product collection + * + * @param Combine $conditions + * @param ProductCollection $productCollection + * @return ProductCollection + * @throws InputException + */ + public function applyConditionsToCollection( + Combine $conditions, + ProductCollection $productCollection + ): ProductCollection { + // rebuild conditions to have only those that we know how to map them to product collection + $mappableConditions = $this->mappableConditionsProcessor->rebuildConditionsTree($conditions); + + // transform conditions to search criteria + $searchCriteria = $this->conditionsToSearchCriteriaMapper->mapConditionsToSearchCriteria($mappableConditions); + + $mappedProductCollection = clone $productCollection; + + // apply search criteria to new version of product collection + $this->searchCriteriaProcessor->process($searchCriteria, $mappedProductCollection); + + return $mappedProductCollection; + } +} diff --git a/app/code/Magento/CatalogRule/Model/Rule.php b/app/code/Magento/CatalogRule/Model/Rule.php index 9ea92e21f09f3..3d98de7fe6768 100644 --- a/app/code/Magento/CatalogRule/Model/Rule.php +++ b/app/code/Magento/CatalogRule/Model/Rule.php @@ -33,6 +33,7 @@ use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Store\Model\StoreManagerInterface; +use Magento\CatalogRule\Model\ResourceModel\Product\ConditionsToCollectionApplier; /** * Catalog Rule data model @@ -159,6 +160,16 @@ class Rule extends \Magento\Rule\Model\AbstractModel implements RuleInterface, I */ protected $ruleConditionConverter; + /** + * @var ConditionsToCollectionApplier + */ + protected $conditionsToCollectionApplier; + + /** + * @var array + */ + private $websitesMap; + /** * @var RuleResourceModel */ @@ -190,6 +201,8 @@ class Rule extends \Magento\Rule\Model\AbstractModel implements RuleInterface, I * @param AttributeValueFactory|null $customAttributeFactory * @param \Magento\Framework\Serialize\Serializer\Json $serializer * @param \Magento\CatalogRule\Model\ResourceModel\RuleResourceModel|null $ruleResourceModel + * @param ConditionsToCollectionApplier $conditionsToCollectionApplier + * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -215,7 +228,8 @@ public function __construct( ExtensionAttributesFactory $extensionFactory = null, AttributeValueFactory $customAttributeFactory = null, Json $serializer = null, - RuleResourceModel $ruleResourceModel = null + RuleResourceModel $ruleResourceModel = null, + ConditionsToCollectionApplier $conditionsToCollectionApplier = null ) { $this->_productCollectionFactory = $productCollectionFactory; $this->_storeManager = $storeManager; @@ -231,6 +245,9 @@ public function __construct( $this->_ruleProductProcessor = $ruleProductProcessor; $this->ruleResourceModel = $ruleResourceModel ?: ObjectManager::getInstance()->get(RuleResourceModel::class); + $this->conditionsToCollectionApplier = $conditionsToCollectionApplier + ?? ObjectManager::getInstance()->get(ConditionsToCollectionApplier::class); + parent::__construct( $context, $registry, @@ -336,6 +353,11 @@ public function getMatchingProductIds() } $this->getConditions()->collectValidatedAttributes($productCollection); + if ($this->canPreMapProducts()) { + $productCollection = $this->conditionsToCollectionApplier + ->applyConditionsToCollection($this->getConditions(), $productCollection); + } + $this->_resourceIterator->walk( $productCollection->getSelect(), [[$this, 'callbackValidateProduct']], @@ -350,6 +372,18 @@ public function getMatchingProductIds() return $this->_productIds; } + private function canPreMapProducts() + { + $conditions = $this->getConditions(); + + // No need to map products if there is no conditions in rule + if (!$conditions || !$conditions->getConditions()) { + return false; + } + + return true; + } + /** * Callback function for product matching * @@ -378,16 +412,19 @@ public function callbackValidateProduct($args) */ protected function _getWebsitesMap() { - $map = []; - $websites = $this->_storeManager->getWebsites(); - foreach ($websites as $website) { - // Continue if website has no store to be able to create catalog rule for website without store - if ($website->getDefaultStore() === null) { - continue; + if ($this->websitesMap === null) { + $this->websitesMap = []; + $websites = $this->_storeManager->getWebsites(); + foreach ($websites as $website) { + // Continue if website has no store to be able to create catalog rule for website without store + if ($website->getDefaultStore() === null) { + continue; + } + $this->websitesMap[$website->getId()] = $website->getDefaultStore()->getId(); } - $map[$website->getId()] = $website->getDefaultStore()->getId(); } - return $map; + + return $this->websitesMap; } /** diff --git a/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php b/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php new file mode 100644 index 0000000000000..7dc832a28ac0e --- /dev/null +++ b/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php @@ -0,0 +1,308 @@ +searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory; + $this->combinedFilterGroupFactory = $combinedFilterGroupFactory; + $this->filterFactory = $filterFactory; + } + + /** + * Maps catalog price rule conditions to search criteria + * + * @param CombinedCondition $conditions + * @return SearchCriteria + * @throws InputException + */ + public function mapConditionsToSearchCriteria(CombinedCondition $conditions): SearchCriteria + { + $filterGroup = $this->mapCombinedConditionToFilterGroup($conditions); + + $searchCriteriaBuilder = $this->searchCriteriaBuilderFactory->create(); + + if ($filterGroup !== null) { + $searchCriteriaBuilder->setFilterGroups([$filterGroup]); + } + + return $searchCriteriaBuilder->create(); + } + + /** + * @param ConditionInterface $condition + * @return null|\Magento\Framework\Api\CombinedFilterGroup|\Magento\Framework\Api\Filter + * @throws InputException + */ + private function mapConditionToFilterGroup(ConditionInterface $condition) + { + if ($condition->getType() === CombinedCondition::class) { + return $this->mapCombinedConditionToFilterGroup($condition); + } elseif ($condition->getType() === SimpleCondition::class) { + return $this->mapSimpleConditionToFilterGroup($condition); + } + + throw new InputException( + __('Undefined condition type "%1" passed in.', $condition->getType()) + ); + } + + /** + * @param Combine $combinedCondition + * @return null|\Magento\Framework\Api\CombinedFilterGroup + * @throws InputException + */ + private function mapCombinedConditionToFilterGroup(CombinedCondition $combinedCondition) + { + $filters = []; + + foreach ($combinedCondition->getConditions() as $condition) { + $filter = $this->mapConditionToFilterGroup($condition); + + if ($filter === null) { + continue; + } + + // This required to solve cases when condition is configured like: + // "If ALL/ANY of these conditions are FALSE" - we need to reverse SQL operator for this "FALSE" + if ((bool)$combinedCondition->getValue() === false) { + $this->reverseSqlOperatorInFilter($filter); + } + + $filters[] = $filter; + } + + if (count($filters) === 0) { + return null; + } + + return $this->createCombinedFilterGroup($filters, $combinedCondition->getAggregator()); + } + + /** + * @param ConditionInterface $productCondition + * @return FilterGroup|Filter + * @throws InputException + */ + private function mapSimpleConditionToFilterGroup(ConditionInterface $productCondition) + { + if (is_array($productCondition->getValue())) { + return $this->processSimpleConditionWithArrayValue($productCondition); + } + + return $this->createFilter( + $productCondition->getAttribute(), + (string) $productCondition->getValue(), + $productCondition->getOperator() + ); + } + + /** + * @param ConditionInterface $productCondition + * @return FilterGroup + * @throws InputException + */ + private function processSimpleConditionWithArrayValue(ConditionInterface $productCondition): FilterGroup + { + $filters = []; + + foreach ($productCondition->getValue() as $subValue) { + $filters[] = $this->createFilter( + $productCondition->getAttribute(), + (string) $subValue, + $productCondition->getOperator() + ); + } + + $combinationMode = $this->getGlueForArrayValues($productCondition->getOperator()); + + return $this->createCombinedFilterGroup($filters, $combinationMode); + } + + /** + * @param string $operator + * @return string + */ + private function getGlueForArrayValues(string $operator): string + { + if (in_array($operator, ['!=', '!{}', '!()'], true)) { + return 'all'; + } + + return 'any'; + } + + /** + * Reverse sql conditions to their corresponding negative analog + * + * @param Filter $filter + * @return void + * @throws InputException + */ + private function reverseSqlOperatorInFilter(Filter $filter) + { + $operatorsMap = [ + 'eq' => 'neq', + 'neq' => 'eq', + 'gteq' => 'lt', + 'lteq' => 'gt', + 'gt' => 'lteq', + 'lt' => 'gteq', + 'like' => 'nlike', + 'nlike' => 'like', + 'in' => 'nin', + 'nin' => 'in', + ]; + + if (!array_key_exists($filter->getConditionType(), $operatorsMap)) { + throw new InputException( + __( + 'Undefined SQL operator "%1" passed in. Valid operators are: %2', + $filter->getConditionType(), + implode(',', array_keys($operatorsMap)) + ) + ); + } + + $filter->setConditionType( + $operatorsMap[$filter->getConditionType()] + ); + } + + /** + * @param array $filters + * @param string $combinationMode + * @return FilterGroup + * @throws InputException + */ + private function createCombinedFilterGroup(array $filters, string $combinationMode): FilterGroup + { + return $this->combinedFilterGroupFactory->create([ + 'data' => [ + FilterGroup::FILTERS => $filters, + FilterGroup::COMBINATION_MODE => $this->mapRuleAggregatorToSQLAggregator($combinationMode) + ] + ]); + } + + /** + * @param string $field + * @param string $value + * @param string $conditionType + * @return Filter + * @throws InputException + */ + private function createFilter(string $field, string $value, string $conditionType): Filter + { + return $this->filterFactory->create([ + 'data' => [ + Filter::KEY_FIELD => $field, + Filter::KEY_VALUE => $value, + Filter::KEY_CONDITION_TYPE => $this->mapRuleOperatorToSQLCondition($conditionType) + ] + ]); + } + + /** + * Maps catalog price rule operators to their corresponding operators in SQL + * + * @param string $ruleOperator + * @return string + * @throws InputException + */ + private function mapRuleOperatorToSQLCondition(string $ruleOperator): string + { + $operatorsMap = [ + '==' => 'eq', // is + '!=' => 'neq', // is not + '>=' => 'gteq', // equals or greater than + '<=' => 'lteq', // equals or less than + '>' => 'gt', // greater than + '<' => 'lt', // less than + '{}' => 'like', // contains + '!{}' => 'nlike', // does not contains + '()' => 'in', // is one of + '!()' => 'nin', // is not one of + ]; + + if (!array_key_exists($ruleOperator, $operatorsMap)) { + throw new InputException( + __( + 'Undefined rule operator "%1" passed in. Valid operators are: %2', + $ruleOperator, + implode(',', array_keys($operatorsMap)) + ) + ); + } + + return $operatorsMap[$ruleOperator]; + } + + /** + * Map rule combine aggregations to corresponding SQL operator + * + * @param string $ruleAggregator + * @return string + * @throws InputException + */ + private function mapRuleAggregatorToSQLAggregator(string $ruleAggregator): string + { + $operatorsMap = [ + 'all' => 'AND', + 'any' => 'OR', + ]; + + if (!array_key_exists(strtolower($ruleAggregator), $operatorsMap)) { + throw new InputException( + __( + 'Undefined rule aggregator "%1" passed in. Valid operators are: %2', + $ruleAggregator, + implode(',', array_keys($operatorsMap)) + ) + ); + } + + return $operatorsMap[$ruleAggregator]; + } +} diff --git a/app/code/Magento/CatalogRule/Model/Rule/Condition/MappableConditionsProcessor.php b/app/code/Magento/CatalogRule/Model/Rule/Condition/MappableConditionsProcessor.php new file mode 100644 index 0000000000000..63c3f62ad0590 --- /dev/null +++ b/app/code/Magento/CatalogRule/Model/Rule/Condition/MappableConditionsProcessor.php @@ -0,0 +1,139 @@ +customConditionProvider = $customConditionProvider; + $this->eavConfig = $eavConfig; + } + + /** + * @param Combine $conditions + * @return Combine + */ + public function rebuildConditionsTree(CombinedCondition $conditions): CombinedCondition + { + return $this->rebuildCombinedCondition($conditions); + } + + /** + * @param Combine $originalConditions + * @return Combine + * @throws InputException + */ + private function rebuildCombinedCondition(CombinedCondition $originalConditions): CombinedCondition + { + $validConditions = []; + $invalidConditions = []; + + foreach ($originalConditions->getConditions() as $condition) { + if ($condition->getType() === CombinedCondition::class) { + $rebuildSubCondition = $this->rebuildCombinedCondition($condition); + + if (count($rebuildSubCondition->getConditions()) > 0) { + $validConditions[] = $rebuildSubCondition; + } else { + $invalidConditions[] = $rebuildSubCondition; + } + + continue; + } + + if ($condition->getType() === SimpleCondition::class) { + if ($this->validateSimpleCondition($condition)) { + $validConditions[] = $condition; + } else { + $invalidConditions[] = $condition; + } + + continue; + } + + throw new InputException( + __('Undefined condition type "%1" passed in.', $condition->getType()) + ); + } + + // if resulted condition group has left no mappable conditions - we can remove it at all + if (count($invalidConditions) > 0 && strtolower($originalConditions->getAggregator()) === 'any') { + $validConditions = []; + } + + $rebuildCondition = clone $originalConditions; + $rebuildCondition->setConditions($validConditions); + + return $rebuildCondition; + } + + /** + * @param Product $originalConditions + * @return bool + */ + private function validateSimpleCondition(SimpleCondition $originalConditions): bool + { + return $this->canUseFieldForMapping($originalConditions->getAttribute()); + } + + /** + * Checks if condition field is mappable + * + * @param string $fieldName + * @return bool + */ + private function canUseFieldForMapping(string $fieldName): bool + { + // We can map field to search criteria if we have custom processor for it + if ($this->customConditionProvider->hasProcessorForField($fieldName)) { + return true; + } + + // Also we can map field to search criteria if it is an EAV attribute + $attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $fieldName); + + // We have this weird check for getBackendType() to verify that attribute really exists + // because due to eavConfig behavior even if pass non existing attribute code we still receive AbstractAttribute + // getAttributeId() is not sufficient too because some attributes don't have it - e.g. attribute_set_id + if ($attribute && $attribute->getBackendType() !== null) { + return true; + } + + // In any other cases we can't map field to search criteria + return false; + } +} diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Rule/Condition/MappableConditionProcessorTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Rule/Condition/MappableConditionProcessorTest.php new file mode 100644 index 0000000000000..1643b3473ae27 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Rule/Condition/MappableConditionProcessorTest.php @@ -0,0 +1,1035 @@ +eavConfigMock = $this->getMockBuilder(EavConfig::class) + ->disableOriginalConstructor() + ->setMethods(['getAttribute']) + ->getMock(); + + $this->customConditionProcessorBuilderMock = $this->getMockBuilder( + CustomConditionProviderInterface::class + )->disableOriginalConstructor() + ->setMethods(['hasProcessorForField']) + ->getMockForAbstractClass(); + + $this->objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->mappableConditionProcessor = $this->objectManagerHelper->getObject( + MappableConditionsProcessor::class, + [ + 'customConditionProvider' => $this->customConditionProcessorBuilderMock, + 'eavConfig' => $this->eavConfigMock, + ] + ); + } + + /** + * input condition tree: + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * condition-2 => [ attribute => field-2 ] + * ] + * ] + * ] + * + * in case when condition-2 is not mappable the result must be next: + * + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => [] + * ] + * ] + */ + public function testConditionV1() + { + $field1 = 'field-1'; + $field2 = 'field-2'; + + $simpleCondition1 = $this->getMockForSimpleCondition($field1); + $simpleCondition2 = $this->getMockForSimpleCondition($field2); + $inputCondition = $this->getMockForCombinedCondition( + [ + $simpleCondition1, + $simpleCondition2 + ], + 'any' + ); + + $validResult = $this->getMockForCombinedCondition([], 'any'); + + $this->customConditionProcessorBuilderMock + ->method('hasProcessorForField') + ->will( + $this->returnValueMap( + [ + [$field1, true], + [$field2, false], + ] + ) + ); + + $this->eavConfigMock + ->method('getAttribute') + ->willReturn(null); + + $result = $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + + $this->assertEquals($validResult, $result); + } + + /** + * input condition tree: + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * condition-2 => [ attribute => field-2 ] + * ] + * ] + * ] + * + * in case when condition-2 is not mappable the result must be next: + * + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * ] + * ] + * ] + */ + public function testConditionV2() + { + $field1 = 'field-1'; + $field2 = 'field-2'; + + $simpleCondition1 = $this->getMockForSimpleCondition($field1); + $simpleCondition2 = $this->getMockForSimpleCondition($field2); + $inputCondition = $this->getMockForCombinedCondition( + [ + $simpleCondition1, + $simpleCondition2 + ], + 'all' + ); + + $validResult = $this->getMockForCombinedCondition( + [ + $simpleCondition1 + ], + 'all' + ); + + $this->customConditionProcessorBuilderMock + ->method('hasProcessorForField') + ->will( + $this->returnValueMap( + [ + [$field1, true], + [$field2, false], + ] + ) + ); + + $this->eavConfigMock + ->method('getAttribute') + ->willReturn(null); + + $result = $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + + $this->assertEquals($validResult, $result); + } + + /** + * input condition tree: + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * condition-2 => [ attribute => field-2 ] + * ] + * ] + * ] + * + * in case when condition-1 and condition-2 are not mappable the result must be next: + * + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => [] + * ] + * ] + */ + public function testConditionV3() + { + $field1 = 'field-1'; + $field2 = 'field-2'; + + $simpleCondition1 = $this->getMockForSimpleCondition($field1); + $simpleCondition2 = $this->getMockForSimpleCondition($field2); + $inputCondition = $this->getMockForCombinedCondition( + [ + $simpleCondition1, + $simpleCondition2 + ], + 'all' + ); + + $validResult = $this->getMockForCombinedCondition([], 'all'); + + $this->customConditionProcessorBuilderMock + ->method('hasProcessorForField') + ->will( + $this->returnValueMap( + [ + [$field1, false], + [$field2, false], + ] + ) + ); + + $this->eavConfigMock + ->method('getAttribute') + ->willReturn(null); + + $result = $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + + $this->assertEquals($validResult, $result); + } + + /** + * input condition tree: + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * condition-2 => [ attribute => field-2 ] + * ] + * ] + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-3 => [ attribute => field-3 ] + * condition-4 => [ attribute => field-4 ] + * ] + * ] + * ] + * ] + * ] + * + * in case when condition-1 is not mappable the result must be next: + * + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * condition-2 => [ attribute => field-2 ] + * ] + * ] + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-3 => [ attribute => field-3 ] + * condition-4 => [ attribute => field-4 ] + * ] + * ] + * ] + * ] + * ] + */ + public function testConditionV4() + { + $field1 = 'field-1'; + $field2 = 'field-2'; + + $simpleCondition1 = $this->getMockForSimpleCondition($field1); + $simpleCondition2 = $this->getMockForSimpleCondition($field2); + $subCondition1 = $this->getMockForCombinedCondition( + [ + $simpleCondition1, + $simpleCondition2 + ], + 'all' + ); + + $field3 = 'field-3'; + $field4 = 'field-4'; + + $simpleCondition3 = $this->getMockForSimpleCondition($field3); + $simpleCondition4 = $this->getMockForSimpleCondition($field4); + $subCondition2 = $this->getMockForCombinedCondition( + [ + $simpleCondition3, + $simpleCondition4 + ], + 'any' + ); + + $inputCondition = $this->getMockForCombinedCondition( + [ + $subCondition1, + $subCondition2 + ], + 'any' + ); + + $validSubCondition1 = $this->getMockForCombinedCondition( + [ + $simpleCondition2 + ], + 'all' + ); + $validSubCondition2 = $this->getMockForCombinedCondition( + [ + $simpleCondition3, + $simpleCondition4 + ], + 'any' + ); + $validResult = $this->getMockForCombinedCondition( + [ + $validSubCondition1, + $validSubCondition2 + ], + 'any' + ); + + $this->customConditionProcessorBuilderMock + ->method('hasProcessorForField') + ->will( + $this->returnValueMap( + [ + [$field1, false], + [$field2, true], + [$field3, true], + [$field4, true], + ] + ) + ); + + $this->eavConfigMock + ->method('getAttribute') + ->willReturn(null); + + $result = $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + + $this->assertEquals($validResult, $result); + } + + /** + * input condition tree: + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * condition-2 => [ attribute => field-2 ] + * ] + * ] + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-3 => [ attribute => field-3 ] + * condition-4 => [ attribute => field-4 ] + * ] + * ] + * ] + * ] + * ] + * + * in case when condition-1 is not mappable the result must be next: + * + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-3 => [ attribute => field-3 ] + * condition-4 => [ attribute => field-4 ] + * ] + * ] + * ] + * ] + * ] + */ + public function testConditionV5() + { + $field1 = 'field-1'; + $field2 = 'field-2'; + + $simpleCondition1 = $this->getMockForSimpleCondition($field1); + $simpleCondition2 = $this->getMockForSimpleCondition($field2); + $subCondition1 = $this->getMockForCombinedCondition( + [ + $simpleCondition1, + $simpleCondition2 + ], + 'any' + ); + + $field3 = 'field-3'; + $field4 = 'field-4'; + + $simpleCondition3 = $this->getMockForSimpleCondition($field3); + $simpleCondition4 = $this->getMockForSimpleCondition($field4); + $subCondition2 = $this->getMockForCombinedCondition( + [ + $simpleCondition3, + $simpleCondition4 + ], + 'any' + ); + + $inputCondition = $this->getMockForCombinedCondition( + [ + $subCondition1, + $subCondition2 + ], + 'all' + ); + + $validSubCondition2 = $this->getMockForCombinedCondition( + [ + $simpleCondition3, + $simpleCondition4 + ], + 'any' + ); + $validResult = $this->getMockForCombinedCondition( + [ + $validSubCondition2 + ], + 'all' + ); + + $this->customConditionProcessorBuilderMock + ->method('hasProcessorForField') + ->will( + $this->returnValueMap( + [ + [$field1, false], + [$field2, true], + [$field3, true], + [$field4, true], + ] + ) + ); + + $this->eavConfigMock + ->method('getAttribute') + ->willReturn(null); + + $result = $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + + $this->assertEquals($validResult, $result); + } + + /** + * input condition tree: + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * condition-2 => [ attribute => field-2 ] + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-3 => [ attribute => field-3 ] + * condition-4 => [ attribute => field-4 ] + * ] + * ] + * ] + * ] + * ] + * + * in case when all condition are mappable there must not be any changes to input + */ + public function testConditionV6() + { + $field1 = 'field-1'; + $field2 = 'field-2'; + + $simpleCondition1 = $this->getMockForSimpleCondition($field1); + $simpleCondition2 = $this->getMockForSimpleCondition($field2); + + $field3 = 'field-3'; + $field4 = 'field-4'; + + $simpleCondition3 = $this->getMockForSimpleCondition($field3); + $simpleCondition4 = $this->getMockForSimpleCondition($field4); + $subCondition1 = $this->getMockForCombinedCondition( + [ + $simpleCondition3, + $simpleCondition4 + ], + 'any' + ); + + $inputCondition = $this->getMockForCombinedCondition( + [ + $simpleCondition1, + $simpleCondition2, + $subCondition1 + ], + 'all' + ); + + $this->customConditionProcessorBuilderMock + ->method('hasProcessorForField') + ->will( + $this->returnValueMap( + [ + [$field1, true], + [$field2, true], + [$field3, true], + [$field4, true], + ] + ) + ); + + $this->eavConfigMock + ->method('getAttribute') + ->willReturn(null); + + $result = $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + + $this->assertEquals($inputCondition, $result); + } + + /** + * input condition tree: + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-2 => [ attribute => field-2 ] + * condition-3 => [ attribute => field-3 ] + * ] + * ] + * ] + * ] + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-4 => [ attribute => field-4 ] + * condition-5 => [ attribute => field-5 ] + * ] + * ] + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-6 => [ attribute => field-6 ] + * condition-7 => [ attribute => field-7 ] + * ] + * ] + * ] + * ] + * ] + * ] + * ] + * + * in case when condition-3 and condition-5 are not mappable the result must be next: + * + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * ] + * ] + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-6 => [ attribute => field-6 ] + * condition-7 => [ attribute => field-7 ] + * ] + * ] + * ] + * ] + * ] + * ] + * ] + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testConditionV7() + { + $field1 = 'field-1'; + $field2 = 'field-2'; + $field3 = 'field-3'; + + $simpleCondition1 = $this->getMockForSimpleCondition($field1); + $simpleCondition2 = $this->getMockForSimpleCondition($field2); + $simpleCondition3 = $this->getMockForSimpleCondition($field3); + + $subCondition1 = $this->getMockForCombinedCondition( + [ + $simpleCondition2, + $simpleCondition3 + ], + 'any' + ); + $subCondition2 = $this->getMockForCombinedCondition( + [ + $simpleCondition1, + $subCondition1 + ], + 'all' + ); + + $field4 = 'field-4'; + $field5 = 'field-5'; + $field6 = 'field-6'; + $field7 = 'field-7'; + + $simpleCondition4 = $this->getMockForSimpleCondition($field4); + $simpleCondition5 = $this->getMockForSimpleCondition($field5); + $simpleCondition6 = $this->getMockForSimpleCondition($field6); + $simpleCondition7 = $this->getMockForSimpleCondition($field7); + + $subCondition3 = $this->getMockForCombinedCondition( + [ + $simpleCondition4, + $simpleCondition5 + ], + 'any' + ); + $subCondition4 = $this->getMockForCombinedCondition( + [ + $simpleCondition6, + $simpleCondition7 + ], + 'any' + ); + $subCondition5 = $this->getMockForCombinedCondition( + [ + $subCondition3, + $subCondition4 + ], + 'all' + ); + + $inputCondition = $this->getMockForCombinedCondition( + [ + $subCondition2, + $subCondition5 + ], + 'any' + ); + + $validSubCondition2 = $this->getMockForCombinedCondition( + [ + $simpleCondition1 + ], + 'all' + ); + $validSubCondition4 = $this->getMockForCombinedCondition( + [ + $subCondition4 + ], + 'all' + ); + + $validResult = $this->getMockForCombinedCondition( + [ + $validSubCondition2, + $validSubCondition4 + ], + 'any' + ); + + $this->customConditionProcessorBuilderMock + ->method('hasProcessorForField') + ->will( + $this->returnValueMap( + [ + [$field1, true], + [$field2, true], + [$field3, false], + [$field4, true], + [$field5, false], + [$field6, true], + [$field7, true], + ] + ) + ); + + $this->eavConfigMock + ->method('getAttribute') + ->willReturn(null); + + $result = $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + + $this->assertEquals($validResult, $result); + } + + /** + * input condition tree: + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * condition-2 => [ attribute => field-2 ] + * ] + * ] + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-3 => [ attribute => field-3 ] + * condition-4 => [ attribute => field-4 ] + * ] + * ] + * ] + * ] + * ] + * + * in case when condition-1 and condition-4 are not mappable the result must be next: + * + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => [] + * ] + */ + public function testConditionV8() + { + $field1 = 'field-1'; + $field2 = 'field-2'; + + $simpleCondition1 = $this->getMockForSimpleCondition($field1); + $simpleCondition2 = $this->getMockForSimpleCondition($field2); + $subCondition1 = $this->getMockForCombinedCondition( + [ + $simpleCondition1, + $simpleCondition2 + ], + 'any' + ); + + $field3 = 'field-3'; + $field4 = 'field-4'; + + $simpleCondition3 = $this->getMockForSimpleCondition($field3); + $simpleCondition4 = $this->getMockForSimpleCondition($field4); + $subCondition2 = $this->getMockForCombinedCondition( + [ + $simpleCondition3, + $simpleCondition4 + ], + 'any' + ); + + $inputCondition = $this->getMockForCombinedCondition( + [ + $subCondition1, + $subCondition2 + ], + 'any' + ); + + $validResult = $this->getMockForCombinedCondition([], 'any'); + + $this->customConditionProcessorBuilderMock + ->method('hasProcessorForField') + ->will( + $this->returnValueMap( + [ + [$field1, false], + [$field2, true], + [$field3, true], + [$field4, false], + ] + ) + ); + + $this->eavConfigMock + ->method('getAttribute') + ->willReturn(null); + + $result = $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + + $this->assertEquals($validResult, $result); + } + + /** + * input condition tree: + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * condition-2 => [ attribute => field-2 ] + * ] + * ] + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-3 => [ attribute => field-3 ] + * condition-4 => [ attribute => field-4 ] + * ] + * ] + * condition-5 => [ attribute => field-5 ] + * ] + * ] + * ] + * + * in case when condition-1 and condition-4 are not mappable the result must be next: + * + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => [] + * ] + */ + public function testConditionV9() + { + $field1 = 'field-1'; + $field2 = 'field-2'; + + $simpleCondition1 = $this->getMockForSimpleCondition($field1); + $simpleCondition2 = $this->getMockForSimpleCondition($field2); + $subCondition1 = $this->getMockForCombinedCondition( + [ + $simpleCondition1, + $simpleCondition2 + ], + 'any' + ); + + $field3 = 'field-3'; + $field4 = 'field-4'; + + $simpleCondition3 = $this->getMockForSimpleCondition($field3); + $simpleCondition4 = $this->getMockForSimpleCondition($field4); + $subCondition2 = $this->getMockForCombinedCondition( + [ + $simpleCondition3, + $simpleCondition4 + ], + 'any' + ); + + $field5 = 'field-5'; + $simpleCondition5 = $this->getMockForSimpleCondition($field5); + + $inputCondition = $this->getMockForCombinedCondition( + [ + $subCondition1, + $subCondition2, + $simpleCondition5 + ], + 'any' + ); + + $validResult = $this->getMockForCombinedCondition([], 'any'); + + $this->customConditionProcessorBuilderMock + ->method('hasProcessorForField') + ->will( + $this->returnValueMap( + [ + [$field1, false], + [$field2, true], + [$field3, true], + [$field4, false], + [$field5, true], + ] + ) + ); + + $this->eavConfigMock + ->method('getAttribute') + ->willReturn(null); + + $result = $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + + $this->assertEquals($validResult, $result); + } + + /** + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage Undefined condition type "olo-lo" passed in. + */ + public function testException() + { + $simpleCondition = $this->getMockForSimpleCondition('field'); + $simpleCondition->setType('olo-lo'); + $inputCondition = $this->getMockForCombinedCondition([$simpleCondition], 'any'); + + $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + } + + protected function getMockForCombinedCondition($subConditions, $aggregator) + { + $mock = $this->getMockBuilder(CombinedCondition::class) + ->disableOriginalConstructor() + ->setMethods() + ->getMock(); + + $mock->setConditions($subConditions); + $mock->setAggregator($aggregator); + $mock->setType(CombinedCondition::class); + + return $mock; + } + + protected function getMockForSimpleCondition($attribute) + { + $mock = $this->getMockBuilder(SimpleCondition::class) + ->disableOriginalConstructor() + ->setMethods() + ->getMock(); + + $mock->setAttribute($attribute); + $mock->setType(SimpleCondition::class); + + return $mock; + } +} diff --git a/app/code/Magento/CatalogRule/etc/di.xml b/app/code/Magento/CatalogRule/etc/di.xml index 4b368b1cef89a..e29f5541fd77c 100644 --- a/app/code/Magento/CatalogRule/etc/di.xml +++ b/app/code/Magento/CatalogRule/etc/di.xml @@ -126,4 +126,27 @@ + + + + Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\ProductCategoryCondition + + + + + + Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\DefaultCondition + CatalogRuleCustomConditionProvider + + + + + CatalogRuleAdvancedFilterProcessor + + + + + CatalogRuleCustomConditionProvider + + diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplierTest.php b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplierTest.php new file mode 100644 index 0000000000000..75bdfc582068c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplierTest.php @@ -0,0 +1,1015 @@ +objectManager = Bootstrap::getObjectManager(); + + $this->productCollectionFactory = $this->objectManager + ->get(\Magento\Catalog\Model\ResourceModel\Product\CollectionFactory::class); + + $this->conditionsToCollectionApplier = $this->objectManager + ->get(\Magento\CatalogRule\Model\ResourceModel\Product\ConditionsToCollectionApplier::class); + + $this->combinedConditionFactory = $this->objectManager + ->get(\Magento\CatalogRule\Model\Rule\Condition\CombineFactory::class); + + $this->simpleConditionFactory = $this->objectManager + ->get(\Magento\CatalogRule\Model\Rule\Condition\ProductFactory::class); + + $this->categoryCollectionFactory = $this->objectManager + ->get(\Magento\Catalog\Model\ResourceModel\Category\CollectionFactory::class); + + $this->setFactory = $this->objectManager + ->get(\Magento\Eav\Model\Entity\Attribute\SetFactory::class); + } + + /** + * @magentoDataFixture Magento/CatalogRule/_files/conditions_to_collection/categories.php + * @magentoDataFixture Magento/CatalogRule/_files/conditions_to_collection/attribute_sets.php + * @magentoDataFixture Magento/CatalogRule/_files/conditions_to_collection/products.php + * + * @magentoDbIsolation disabled + */ + public function testVariations() + { + foreach ($this->conditionProvider() as $variationName => $variationData) { + $condition = $variationData['condition']; + $expectedSkuList = $variationData['expected-sku']; + + $productCollection = $this->productCollectionFactory->create(); + $resultCollection = $this->conditionsToCollectionApplier + ->applyConditionsToCollection($condition, $productCollection); + + $resultSkuList = array_map( + function (Product $product) { + return $product->getSku(); + }, + array_values($resultCollection->getItems()) + ); + + asort($expectedSkuList); + asort($resultSkuList); + + $this->assertEquals($expectedSkuList, $resultSkuList, sprintf('%s failed', $variationName)); + } + } + + /** + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage Undefined rule operator "====" passed in. Valid operators are: ==,!=,>=,<=,>,<,{},!{},(),!() + * + * @magentoDbIsolation disabled + */ + public function testExceptionUndefinedRuleOperator() + { + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'all', + 'value' => 0, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '====', + 'value' => 42, + 'attribute' => 'attribute_set_id' + ] + ] + ]; + + $combineCondition = $this->getCombineConditionFromArray($conditions); + + $productCollection = $this->productCollectionFactory->create(); + $this->conditionsToCollectionApplier + ->applyConditionsToCollection($combineCondition, $productCollection); + } + + /** + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage Undefined rule aggregator "olo-lo" passed in. Valid operators are: all,any + * + * @magentoDbIsolation disabled + */ + public function testExceptionUndefinedRuleAggregator() + { + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'olo-lo', + 'value' => 0, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => 42, + 'attribute' => 'attribute_set_id' + ] + ] + ]; + + $combineCondition = $this->getCombineConditionFromArray($conditions); + + $productCollection = $this->productCollectionFactory->create(); + $this->conditionsToCollectionApplier + ->applyConditionsToCollection($combineCondition, $productCollection); + } + + /** + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + private function conditionProvider() + { + return [ + // test filter by category without children + 'variation 1' => [ + 'condition' => $this->getConditionsForVariation1(), + 'expected-sku' => [ + 'simple-product-1', + 'simple-product-2', + 'simple-product-7', + 'simple-product-8' + ] + ], + + // test filter by root category + 'variation 2' => [ + 'condition' => $this->getConditionsForVariation2(), + 'expected-sku' => [ + 'simple-product-1', + 'simple-product-2', + 'simple-product-3', + 'simple-product-4', + 'simple-product-5', + 'simple-product-6', + 'simple-product-7', + 'simple-product-8', + 'simple-product-9', + 'simple-product-10', + 'simple-product-11', + 'simple-product-12' + ] + ], + + // test filter by anchor category with children + 'variation 3' => [ + 'condition' => $this->getConditionsForVariation3(), + 'expected-sku' => [ + 'simple-product-1', + 'simple-product-2', + 'simple-product-3', + 'simple-product-4', + 'simple-product-5', + 'simple-product-6', + 'simple-product-9', + 'simple-product-10', + 'simple-product-11', + 'simple-product-12' + ] + ], + + // test filter by non existing category + 'variation 4' => [ + 'condition' => $this->getConditionsForVariation4(), + 'expected-sku' => [] + ], + + // test filter by sku + 'variation 5' => [ + 'condition' => $this->getConditionsForVariation5(), + 'expected-sku' => ['simple-product-2'] + ], + + // test filter by attribute set + 'variation 6' => [ + 'condition' => $this->getConditionsForVariation6(), + 'expected-sku' => [ + 'simple-product-1', + 'simple-product-4', + 'simple-product-7', + 'simple-product-10' + ] + ], + + // test filter by product name + 'variation 7' => [ + 'condition' => $this->getConditionsForVariation7(), + 'expected-sku' => [ + 'simple-product-1', + 'simple-product-9', + 'simple-product-12' + ] + ], + + // test filter by not existing attribute + 'variation 8' => [ + 'condition' => $this->getConditionsForVariation8(), + 'expected-sku' => [ + 'simple-product-1', + 'simple-product-2', + 'simple-product-3', + 'simple-product-4', + 'simple-product-5', + 'simple-product-6', + 'simple-product-7', + 'simple-product-8', + 'simple-product-9', + 'simple-product-10', + 'simple-product-11', + 'simple-product-12' + ] + ], + + // test filter by category with empty value + 'variation 9' => [ + 'condition' => $this->getConditionsForVariation9(), + 'expected-sku' => [] + ], + + // test filter by sku with empty value + 'variation 10' => [ + 'condition' => $this->getConditionsForVariation10(), + 'expected-sku' => [ + 'simple-product-1', + 'simple-product-2', + 'simple-product-3', + 'simple-product-4', + 'simple-product-5', + 'simple-product-6', + 'simple-product-7', + 'simple-product-8', + 'simple-product-9', + 'simple-product-10', + 'simple-product-11', + 'simple-product-12' + ] + ], + + // test filter by name with empty value + 'variation 11' => [ + 'condition' => $this->getConditionsForVariation11(), + 'expected-sku' => [] + ], + + // test filter by like condition + 'variation 12' => [ + 'condition' => $this->getConditionsForVariation12(), + 'expected-sku' => [ + 'simple-product-1', + 'simple-product-2', + 'simple-product-3', + 'simple-product-4', + 'simple-product-5', + 'simple-product-6', + 'simple-product-9', + 'simple-product-10', + 'simple-product-11', + 'simple-product-12' + ] + ], + + // test filter with ALL aggregation + 'variation 13' => [ + 'condition' => $this->getConditionsForVariation13(), + 'expected-sku' => [ + 'simple-product-7', + 'simple-product-8' + ] + ], + + // test filter with ANY aggregation + 'variation 14' => [ + 'condition' => $this->getConditionsForVariation14(), + 'expected-sku' => [ + 'simple-product-1', + 'simple-product-2', + 'simple-product-3', + 'simple-product-4', + 'simple-product-7', + 'simple-product-8' + ] + ], + + // test filter with array in product condition's value + 'variation 15' => [ + 'condition' => $this->getConditionsForVariation15(), + 'expected-sku' => [ + 'simple-product-1', + 'simple-product-2', + 'simple-product-3', + 'simple-product-4', + 'simple-product-7', + 'simple-product-8' + ] + ], + + // test filter by multiple sku + 'variation 16' => [ + 'condition' => $this->getConditionsForVariation16(), + 'expected-sku' => [ + 'simple-product-1', + 'simple-product-5', + 'simple-product-11' + ] + ], + + // test filter with multiple combined conditions + 'variation 17' => [ + 'condition' => $this->getConditionsForVariation17(), + 'expected-sku' => [ + 'simple-product-1', + 'simple-product-2', + 'simple-product-4', + 'simple-product-8', + 'simple-product-10' + ] + ], + + // test filter with multiply levels in conditions + 'variation 18' => [ + 'condition' => $this->getConditionsForVariation18(), + 'expected-sku' => [ + 'simple-product-1', + 'simple-product-2', + 'simple-product-3', + 'simple-product-4', + 'simple-product-5', + 'simple-product-6', + 'simple-product-10', + 'simple-product-11' + ] + ], + + // test filter with empty conditions + 'variation 19' => [ + 'condition' => $this->getConditionsForVariation19(), + 'expected-sku' => [ + 'simple-product-1', + 'simple-product-2', + 'simple-product-3', + 'simple-product-4', + 'simple-product-5', + 'simple-product-6', + 'simple-product-7', + 'simple-product-8', + 'simple-product-9', + 'simple-product-10', + 'simple-product-11', + 'simple-product-12' + ] + ], + + // test filter for case "If ALL of these conditions are FALSE" + 'variation 20' => [ + 'condition' => $this->getConditionsForVariation20(), + 'expected-sku' => [ + 'simple-product-2', + 'simple-product-5', + 'simple-product-8', + 'simple-product-11' + ] + ], + + // test filter for case "If ANY of these conditions are FALSE" + 'variation 21' => [ + 'condition' => $this->getConditionsForVariation21(), + 'expected-sku' => [ + 'simple-product-1', + 'simple-product-2', + 'simple-product-3', + 'simple-product-4', + 'simple-product-5', + 'simple-product-6', + 'simple-product-7', + 'simple-product-8', + 'simple-product-9', + 'simple-product-10', + 'simple-product-11', + 'simple-product-12' + ] + ], + ]; + } + + private function getConditionsForVariation1() + { + $category2Name = 'Category 2'; + + $category2Id = $this->categoryCollectionFactory + ->create() + ->addAttributeToFilter('name', $category2Name) + ->getAllIds(); + + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'any', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => implode(',', $category2Id), + 'attribute' => 'category_ids' + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation2() + { + $categoryName = 'Default Category'; + + $categoryId = $this->categoryCollectionFactory + ->create() + ->addAttributeToFilter('name', $categoryName) + ->getAllIds(); + + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'any', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => implode(',', $categoryId), + 'attribute' => 'category_ids' + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation3() + { + $category1Name = 'Category 1'; + + $category1Id = $this->categoryCollectionFactory + ->create() + ->addAttributeToFilter('name', $category1Name) + ->getAllIds(); + + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'any', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => implode(',', $category1Id), + 'attribute' => 'category_ids' + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation4() + { + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'any', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => implode(',', [308567758103]), + 'attribute' => 'category_ids' + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation5() + { + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'any', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '{}', + 'value' => 'product-2', + 'attribute' => 'sku' + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation6() + { + $attrSet = $this->setFactory->create() + ->load('Super Powerful Muffins', 'attribute_set_name'); + + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'any', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => $attrSet->getId(), + 'attribute' => 'attribute_set_id' + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation7() + { + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'any', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '{}', + 'value' => 'Sale', + 'attribute' => 'name' + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation8() + { + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'any', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => 'Sale', + 'attribute' => 'absolutely_random_attribute_name' + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation9() + { + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'any', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => '', + 'attribute' => 'category_ids' + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation10() + { + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'any', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => '', + 'attribute' => 'sku' + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation11() + { + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'any', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => '', + 'attribute' => 'name' + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation12() + { + $category1Name = 'Category 1'; + + $category1Id = $this->categoryCollectionFactory + ->create() + ->addAttributeToFilter('name', $category1Name) + ->getAllIds(); + + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'any', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '{}', + 'value' => implode(',', $category1Id), + 'attribute' => 'category_ids' + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation13() + { + $category3Name = 'Category 3'; + + $category3Id = $this->categoryCollectionFactory + ->create() + ->addAttributeToFilter('name', $category3Name) + ->getAllIds(); + + $category2Name = 'Category 2'; + + $category2Id = $this->categoryCollectionFactory + ->create() + ->addAttributeToFilter('name', $category2Name) + ->getAllIds(); + + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'all', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '{}', + 'value' => implode(',', $category3Id), + 'attribute' => 'category_ids' + ], + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => implode(',', $category2Id), + 'attribute' => 'category_ids' + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation14() + { + $category3Name = 'Category 3'; + + $category3Id = $this->categoryCollectionFactory + ->create() + ->addAttributeToFilter('name', $category3Name) + ->getAllIds(); + + $category2Name = 'Category 2'; + + $category2Id = $this->categoryCollectionFactory + ->create() + ->addAttributeToFilter('name', $category2Name) + ->getAllIds(); + + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'any', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '{}', + 'value' => implode(',', $category3Id), + 'attribute' => 'category_ids' + ], + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => implode(',', $category2Id), + 'attribute' => 'category_ids' + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation15() + { + $category3Name = 'Category 3'; + + $category3Id = $this->categoryCollectionFactory + ->create() + ->addAttributeToFilter('name', $category3Name) + ->getAllIds(); + + $category2Name = 'Category 2'; + + $category2Id = $this->categoryCollectionFactory + ->create() + ->addAttributeToFilter('name', $category2Name) + ->getAllIds(); + + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'all', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '{}', + 'value' => [$category3Id[0], $category2Id[0]], + 'attribute' => 'category_ids' + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation16() + { + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'any', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '()', + 'value' => 'simple-product-1,simple-product-5,simple-product-11', + 'attribute' => 'sku' + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation17() + { + $category1Name = 'Category 1'; + + $category1Id = $this->categoryCollectionFactory + ->create() + ->addAttributeToFilter('name', $category1Name) + ->getAllIds(); + + $category2Name = 'Category 2'; + + $category2Id = $this->categoryCollectionFactory + ->create() + ->addAttributeToFilter('name', $category2Name) + ->getAllIds(); + + $attributeSetMuffins = $this->setFactory->create() + ->load('Super Powerful Muffins', 'attribute_set_name'); + + $attributeSetRangers = $this->setFactory->create() + ->load('Banana Rangers', 'attribute_set_name'); + + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'any', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'all', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => implode(',', $category1Id), + 'attribute' => 'category_ids' + ], + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => $attributeSetMuffins->getId(), + 'attribute' => 'attribute_set_id' + ] + ] + ], + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'all', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => implode(',', $category2Id), + 'attribute' => 'category_ids' + ], + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => $attributeSetRangers->getId(), + 'attribute' => 'attribute_set_id' + ] + ] + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation18() + { + $category1Name = 'Category 1'; + + $category1Id = $this->categoryCollectionFactory + ->create() + ->addAttributeToFilter('name', $category1Name) + ->getAllIds(); + + $attributeSetMuffins = $this->setFactory->create() + ->load('Super Powerful Muffins', 'attribute_set_name'); + + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'any', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'all', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => implode(',', $category1Id), + 'attribute' => 'category_ids' + ], + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'any', + 'value' => 1, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '!{}', + 'value' => '(Sale)', + 'attribute' => 'name' + ], + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => $attributeSetMuffins->getId(), + 'attribute' => 'attribute_set_id' + ], + ] + ] + ] + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation19() + { + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'all', + 'value' => 0, + 'conditions' => [] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation20() + { + $attributeSetMuffins = $this->setFactory->create() + ->load('Super Powerful Muffins', 'attribute_set_name'); + + $attributeSetGuardians = $this->setFactory->create() + ->load('Guardians of the Refrigerator', 'attribute_set_name'); + + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'all', + 'value' => 0, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => $attributeSetMuffins->getId(), + 'attribute' => 'attribute_set_id' + ], + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => $attributeSetGuardians->getId(), + 'attribute' => 'attribute_set_id' + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getConditionsForVariation21() + { + $attributeSetMuffins = $this->setFactory->create() + ->load('Super Powerful Muffins', 'attribute_set_name'); + + $attributeSetGuardians = $this->setFactory->create() + ->load('Guardians of the Refrigerator', 'attribute_set_name'); + + $conditions = [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Combine::class, + 'aggregator' => 'any', + 'value' => 0, + 'conditions' => [ + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => $attributeSetMuffins->getId(), + 'attribute' => 'attribute_set_id' + ], + [ + 'type' => \Magento\CatalogRule\Model\Rule\Condition\Product::class, + 'operator' => '==', + 'value' => $attributeSetGuardians->getId(), + 'attribute' => 'attribute_set_id' + ] + ] + ]; + + return $this->getCombineConditionFromArray($conditions); + } + + private function getCombineConditionFromArray(array $data) + { + $combinedCondition = $this->combinedConditionFactory->create(); + $combinedCondition->setPrefix('conditions'); + $combinedCondition->loadArray($data); + + return $combinedCondition; + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/attribute_sets.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/attribute_sets.php new file mode 100644 index 0000000000000..45999a7279814 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/attribute_sets.php @@ -0,0 +1,50 @@ +get(\Magento\Eav\Api\Data\AttributeSetInterfaceFactory::class); +$dataObjectHelper = $objectManager->get(\Magento\Framework\Api\DataObjectHelper::class); +$attributeSetRepository = $objectManager->get(\Magento\Catalog\Api\AttributeSetRepositoryInterface::class); +$attributeSetManagement = $objectManager->get(\Magento\Eav\Api\AttributeSetManagementInterface::class); + +$entityTypeId = $objectManager->create(\Magento\Eav\Model\Entity\Type::class)->loadByCode('catalog_product')->getId(); +$defaultAttributeSet = $objectManager->get(Magento\Eav\Model\Config::class) + ->getEntityType('catalog_product') + ->getDefaultAttributeSetId(); + +$attributeSet = $attributeSetFactory->create(); +$dataObjectHelper->populateWithArray( + $attributeSet, + [ + 'attribute_set_name' => 'Super Powerful Muffins', + 'entity_type_id' => $entityTypeId, + ], + \Magento\Eav\Api\Data\AttributeSetInterface::class +); +$attributeSetManagement->create('catalog_product', $attributeSet, $defaultAttributeSet)->save(); + + +$attributeSet = $attributeSetFactory->create(); +$dataObjectHelper->populateWithArray( + $attributeSet, + [ + 'attribute_set_name' => 'Banana Rangers', + 'entity_type_id' => $entityTypeId, + ], + \Magento\Eav\Api\Data\AttributeSetInterface::class +); +$attributeSetManagement->create('catalog_product', $attributeSet, $defaultAttributeSet)->save(); + +$attributeSet = $attributeSetFactory->create(); +$dataObjectHelper->populateWithArray( + $attributeSet, + [ + 'attribute_set_name' => 'Guardians of the Refrigerator', + 'entity_type_id' => $entityTypeId, + ], + \Magento\Eav\Api\Data\AttributeSetInterface::class +); +$attributeSetManagement->create('catalog_product', $attributeSet, $defaultAttributeSet)->save(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/attribute_sets_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/attribute_sets_rollback.php new file mode 100644 index 0000000000000..23afac12b9fdd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/attribute_sets_rollback.php @@ -0,0 +1,25 @@ +create(\Magento\Eav\Model\Entity\Attribute\Set::class) + ->load('Super Powerful Muffins', 'attribute_set_name'); +if ($attributeSet->getId()) { + $attributeSet->delete(); +} + +$attributeSet = $objectManager->create(\Magento\Eav\Model\Entity\Attribute\Set::class) + ->load('Banana Rangers', 'attribute_set_name'); +if ($attributeSet->getId()) { + $attributeSet->delete(); +} + +$attributeSet = $objectManager->create(\Magento\Eav\Model\Entity\Attribute\Set::class) + ->load('Guardians of the Refrigerator', 'attribute_set_name'); +if ($attributeSet->getId()) { + $attributeSet->delete(); +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/categories.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/categories.php new file mode 100644 index 0000000000000..485f2895b2f9b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/categories.php @@ -0,0 +1,83 @@ +create(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); + +/** @var $category \Magento\Catalog\Model\Category */ +$category1 = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category1->isObjectNew(true); +$category1 + ->setName('Category 1') + ->setParentId(2) + ->setPath('1/2') + ->setLevel(2) + ->setIsActive(true) + ->setIsAnchor(true) + ->setPosition(1) + ->save(); + +/** @var $category \Magento\Catalog\Model\Category */ +$category1_1 = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category1_1->isObjectNew(true); +$category1_1 + ->setName('Category 1.1') + ->setParentId($category1->getId()) + ->setPath($category1->getPath()) + ->setLevel(3) + ->setIsActive(true) + ->setIsAnchor(true) + ->setPosition(1) + ->save(); + +$category1_2 = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category1_2->isObjectNew(true); +$category1_2 + ->setName('Category 1.2') + ->setParentId($category1->getId()) + ->setPath($category1->getPath()) + ->setLevel(3) + ->setIsActive(true) + ->setIsAnchor(true) + ->setPosition(2) + ->save(); + +/** @var $category \Magento\Catalog\Model\Category */ +$category1_1_1 = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category1_1_1->isObjectNew(true); +$category1_1_1 + ->setName('Category 1.1.1') + ->setParentId($category1_1->getId()) + ->setPath($category1_1->getPath()) + ->setLevel(4) + ->setIsActive(true) + ->setPosition(1) + ->save(); + +/** @var $category \Magento\Catalog\Model\Category */ +$category2 = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category2->isObjectNew(true); +$category2 + ->setName('Category 2') + ->setParentId(2) + ->setPath('1/2') + ->setLevel(2) + ->setIsActive(true) + ->setPosition(2) + ->save(); + +/** @var $category \Magento\Catalog\Model\Category */ +$category3 = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category3->isObjectNew(true); +$category3 + ->setName('Category 3') + ->setParentId(2) + ->setPath('1/2') + ->setLevel(2) + ->setIsActive(true) + ->setPosition(8) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/categories_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/categories_rollback.php new file mode 100644 index 0000000000000..04d42692cea7a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/categories_rollback.php @@ -0,0 +1,28 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +//Remove categories +/** @var Magento\Catalog\Model\ResourceModel\Category\Collection $collection */ +$collection = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); +$collection->addAttributeToFilter('level', ['gt' => 1]); + +foreach ($collection as $category) { + /** @var \Magento\Catalog\Model\Category $category */ + if ($category->getLevel() !== 1) { + $category->delete(); + } +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products.php new file mode 100644 index 0000000000000..8b52f2604b531 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products.php @@ -0,0 +1,206 @@ +create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +$attributeSetMuffins = $objectManager->create(\Magento\Eav\Model\Entity\Attribute\Set::class) + ->load('Super Powerful Muffins', 'attribute_set_name'); +$attributeSetRangers = $objectManager->create(\Magento\Eav\Model\Entity\Attribute\Set::class) + ->load('Banana Rangers', 'attribute_set_name'); +$attributeSetGuardians = $objectManager->create(\Magento\Eav\Model\Entity\Attribute\Set::class) + ->load('Guardians of the Refrigerator', 'attribute_set_name'); + +$productsData = [ + [ + 'type-id' => 'simple', + 'attribute-set-id' => $attributeSetMuffins->getId(), + 'website-ids' => [1], + 'name' => 'Simple Product 1 (Sale)', + 'sku' => 'simple-product-1', + 'price' => 10, + 'visibility' => \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH, + 'status' => \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED, + 'stock-data' => ['use_config_manage_stock' => 1, 'qty' => 77, 'is_in_stock' => 1], + 'qty' => 42, + 'categories' => ['Category 1', 'Category 2'], + ], + [ + 'type-id' => 'simple', + 'attribute-set-id' => $attributeSetRangers->getId(), + 'website-ids' => [1], + 'name' => 'Simple Product 2', + 'sku' => 'simple-product-2', + 'price' => 10, + 'visibility' => \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH, + 'status' => \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED, + 'stock-data' => ['use_config_manage_stock' => 1, 'qty' => 22, 'is_in_stock' => 1], + 'qty' => 42, + 'categories' => ['Category 1', 'Category 2'], + ], + [ + 'type-id' => 'simple', + 'attribute-set-id' => $attributeSetGuardians->getId(), + 'website-ids' => [1], + 'name' => 'Simple Product 3', + 'sku' => 'simple-product-3', + 'price' => 10, + 'visibility' => \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH, + 'status' => \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED, + 'stock-data' => ['use_config_manage_stock' => 1, 'qty' => 100, 'is_in_stock' => 1], + 'qty' => 42, + 'categories' => ['Category 1.1', 'Category 3'], + ], + [ + 'type-id' => 'simple', + 'attribute-set-id' => $attributeSetMuffins->getId(), + 'website-ids' => [1], + 'name' => 'Simple Product 4', + 'sku' => 'simple-product-4', + 'price' => 10, + 'visibility' => \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH, + 'status' => \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED, + 'stock-data' => ['use_config_manage_stock' => 1, 'qty' => 22, 'is_in_stock' => 1], + 'qty' => 42, + 'categories' => ['Category 1.1', 'Category 3'], + ], + [ + 'type-id' => 'simple', + 'attribute-set-id' => $attributeSetRangers->getId(), + 'website-ids' => [1], + 'name' => 'Simple Product 5', + 'sku' => 'simple-product-5', + 'price' => 10, + 'visibility' => \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH, + 'status' => \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED, + 'stock-data' => ['use_config_manage_stock' => 1, 'qty' => 22, 'is_in_stock' => 1], + 'qty' => 42, + 'categories' => ['Category 1.2', 'Category 1.1.1'], + ], + [ + 'type-id' => 'simple', + 'attribute-set-id' => $attributeSetGuardians->getId(), + 'website-ids' => [1], + 'name' => 'Simple Product 6', + 'sku' => 'simple-product-6', + 'price' => 10, + 'visibility' => \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH, + 'status' => \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED, + 'stock-data' => ['use_config_manage_stock' => 1, 'qty' => 97, 'is_in_stock' => 1], + 'qty' => 42, + 'categories' => ['Category 1.2', 'Category 1.1.1'], + ], + [ + 'type-id' => 'simple', + 'attribute-set-id' => $attributeSetMuffins->getId(), + 'website-ids' => [1], + 'name' => 'Simple Product 7', + 'sku' => 'simple-product-7', + 'price' => 10, + 'visibility' => \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH, + 'status' => \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED, + 'stock-data' => ['use_config_manage_stock' => 1, 'qty' => 22, 'is_in_stock' => 1], + 'qty' => 42, + 'categories' => ['Category 3', 'Category 2'], + ], + [ + 'type-id' => 'simple', + 'attribute-set-id' => $attributeSetRangers->getId(), + 'website-ids' => [1], + 'name' => 'Simple Product 8', + 'sku' => 'simple-product-8', + 'price' => 10, + 'visibility' => \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH, + 'status' => \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED, + 'stock-data' => ['use_config_manage_stock' => 1, 'qty' => 22, 'is_in_stock' => 1], + 'qty' => 42, + 'categories' => ['Category 3', 'Category 2'], + ], + [ + 'type-id' => 'simple', + 'attribute-set-id' => $attributeSetGuardians->getId(), + 'website-ids' => [1], + 'name' => 'Simple Product 9 (Sale)', + 'sku' => 'simple-product-9', + 'price' => 10, + 'visibility' => \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH, + 'status' => \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED, + 'stock-data' => ['use_config_manage_stock' => 1, 'qty' => 22, 'is_in_stock' => 1], + 'qty' => 42, + 'categories' => ['Category 1.1', 'Category 1.2'], + ], + [ + 'type-id' => 'simple', + 'attribute-set-id' => $attributeSetMuffins->getId(), + 'website-ids' => [1], + 'name' => 'Simple Product 10', + 'sku' => 'simple-product-10', + 'price' => 10, + 'visibility' => \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH, + 'status' => \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED, + 'stock-data' => ['use_config_manage_stock' => 1, 'qty' => 22, 'is_in_stock' => 1], + 'qty' => 42, + 'categories' => ['Category 1.1', 'Category 1.2'], + ], + [ + 'type-id' => 'simple', + 'attribute-set-id' => $attributeSetRangers->getId(), + 'website-ids' => [1], + 'name' => 'Simple Product 11', + 'sku' => 'simple-product-11', + 'price' => 10, + 'visibility' => \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH, + 'status' => \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED, + 'stock-data' => ['use_config_manage_stock' => 1, 'qty' => 22, 'is_in_stock' => 1], + 'qty' => 42, + 'categories' => ['Category 1.1.1'], + ], + [ + 'type-id' => 'simple', + 'attribute-set-id' => $attributeSetGuardians->getId(), + 'website-ids' => [1], + 'name' => 'Simple Product 12 (Sale)', + 'sku' => 'simple-product-12', + 'price' => 10, + 'visibility' => \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH, + 'status' => \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED, + 'stock-data' => ['use_config_manage_stock' => 1, 'qty' => 22, 'is_in_stock' => 1], + 'qty' => 42, + 'categories' => ['Category 1.1.1'], + ], +]; + +foreach ($productsData as $productData) { + $categoriesIds = []; + + foreach ($productData['categories'] as $category) { + /** @var \Magento\Catalog\Model\ResourceModel\Category\Collection $categoryCollection */ + $categoryCollection = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); + $categoryCollection->addAttributeToFilter('name', $category); + + array_push($categoriesIds, ...$categoryCollection->getAllIds()); + } + + /** @var $product \Magento\Catalog\Model\Product */ + $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Model\Product::class); + + $product + ->setTypeId($productData['type-id']) + ->setAttributeSetId($productData['attribute-set-id']) + ->setWebsiteIds($productData['website-ids']) + ->setName($productData['name']) + ->setSku($productData['sku']) + ->setPrice($productData['price']) + ->setVisibility($productData['visibility']) + ->setStatus($productData['status']) + ->setStockData($productData['stock-data']) + ->setQty($productData['qty']) + ->setCategoryIds($categoriesIds); + + $productRepository->save($product); +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products_rollback.php new file mode 100644 index 0000000000000..0e591d20f8a6a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products_rollback.php @@ -0,0 +1,23 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$collection = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); + +foreach ($collection as $product) { + /** @var \Magento\Catalog\Model\Product $category */ + $product->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/lib/internal/Magento/Framework/Api/CombinedFilterGroup.php b/lib/internal/Magento/Framework/Api/CombinedFilterGroup.php new file mode 100644 index 0000000000000..21d95b72fed1c --- /dev/null +++ b/lib/internal/Magento/Framework/Api/CombinedFilterGroup.php @@ -0,0 +1,78 @@ +_get(self::FILTERS); + return $filters === null ? [] : $filters; + } + + /** + * Set filters + * + * @param \Magento\Framework\Api\Filter[] $filters + * @return $this + * @codeCoverageIgnore + */ + public function setFilters(array $filters = null): self + { + return $this->setData(self::FILTERS, $filters); + } + + /** + * @return mixed|null + */ + public function getCombinationMode() + { + return $this->_get(self::COMBINATION_MODE); + } + + /** + * @param string $mode + * @return $this + * @throws InputException + */ + public function setCombinationMode(string $mode): self + { + if ($mode !== self::COMBINED_WITH_AND && $mode !== self::COMBINED_WITH_OR) { + throw new InputException( + new Phrase('Invalid combination mode: %1', [$mode]) + ); + } + + return $this->setData(self::COMBINATION_MODE, $mode); + } +} diff --git a/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/AdvancedFilterProcessor.php b/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/AdvancedFilterProcessor.php new file mode 100644 index 0000000000000..6f94407200e40 --- /dev/null +++ b/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/AdvancedFilterProcessor.php @@ -0,0 +1,131 @@ +defaultConditionProcessor = $defaultConditionProcessor; + $this->conditionManager = $conditionManager; + $this->customConditionProvider = $customConditionProvider; + } + + /** + * Apply Search Criteria Filters to collection + * + * @param SearchCriteriaInterface $searchCriteria + * @param AbstractDb $collection + * @return void + */ + public function process(SearchCriteriaInterface $searchCriteria, AbstractDb $collection) + { + foreach ($searchCriteria->getFilterGroups() as $group) { + $conditions = $this->getConditionsFromFilterGroup($group); + $collection->getSelect()->where($conditions); + } + } + + /** + * Add FilterGroup to the collection + * + * @param CombinedFilterGroup $filterGroup + * @return string + * @throws InputException + */ + private function getConditionsFromFilterGroup(CombinedFilterGroup $filterGroup): string + { + $conditions = []; + + foreach ($filterGroup->getFilters() as $filter) { + if ($filter instanceof CombinedFilterGroup) { + $conditions[] = $this->getConditionsFromFilterGroup($filter); + continue; + } + + if ($filter instanceof Filter) { + $conditions[] = $this->getConditionsFromFilter($filter); + continue; + } + + throw new InputException( + new Phrase('Undefined filter group "%1" passed in.', [get_class($filter)]) + ); + } + + return $this->conditionManager->wrapBrackets( + $this->conditionManager->combineQueries($conditions, $filterGroup->getCombinationMode()) + ); + } + + /** + * @param Filter $filter + * @return string + * @throws InputException + */ + private function getConditionsFromFilter(Filter $filter): string + { + if ($this->customConditionProvider->hasProcessorForField($filter->getField())) { + $customProcessor = $this->customConditionProvider->getProcessorByField($filter->getField()); + return $customProcessor->build($filter); + } + + return $this->defaultConditionProcessor->build($filter); + } +} diff --git a/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/CustomConditionInterface.php b/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/CustomConditionInterface.php new file mode 100644 index 0000000000000..8e03d01286ff6 --- /dev/null +++ b/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/CustomConditionInterface.php @@ -0,0 +1,34 @@ +customConditionProcessors = $customConditionProcessors; + } + + /** + * Get custom processor by field name + * + * @param string $fieldName + * @return CustomConditionInterface + * @throws InputException + */ + public function getProcessorByField(string $fieldName): CustomConditionInterface + { + if (!$this->hasProcessorForField($fieldName)) { + throw new InputException( + new Phrase('Custom processor for field "%1" is absent.', [$fieldName]) + ); + } + + return $this->customConditionProcessors[$fieldName]; + } + + /** + * Check if collection has custom processor for given field name + * + * @param string $fieldName + * @return bool + */ + public function hasProcessorForField(string $fieldName): bool + { + return array_key_exists($fieldName, $this->customConditionProcessors); + } +} diff --git a/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/CustomConditionProviderInterface.php b/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/CustomConditionProviderInterface.php new file mode 100644 index 0000000000000..504db29c2a184 --- /dev/null +++ b/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/CustomConditionProviderInterface.php @@ -0,0 +1,37 @@ + CustomConditionInterface + * You can use di.xml to configure with any custom conditions you need + */ +interface CustomConditionProviderInterface +{ + /** + * Get custom processor by field name + * + * @param string $fieldName + * @return CustomConditionInterface + * @throws InputException + */ + public function getProcessorByField(string $fieldName): CustomConditionInterface; + + /** + * Check if collection has custom processor for given field name + * + * @param string $fieldName + * @return bool + */ + public function hasProcessorForField(string $fieldName): bool; +} diff --git a/lib/internal/Magento/Framework/Api/Test/Unit/SearchCriteria/CollectionProcessor/ConditionProcessor/CustomConditionProviderTest.php b/lib/internal/Magento/Framework/Api/Test/Unit/SearchCriteria/CollectionProcessor/ConditionProcessor/CustomConditionProviderTest.php new file mode 100644 index 0000000000000..446eafb2cb414 --- /dev/null +++ b/lib/internal/Magento/Framework/Api/Test/Unit/SearchCriteria/CollectionProcessor/ConditionProcessor/CustomConditionProviderTest.php @@ -0,0 +1,91 @@ +customConditionMock = $this->getMockBuilder(CustomConditionInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->customConditionProcessorBuilder = $objectManagerHelper + ->getObject( + CustomConditionProvider::class, + [ + 'customConditionProcessors' => [ + 'my-valid-field' => $this->customConditionMock, + ] + ] + ); + } + + public function testPositiveHasProcessorForField() + { + $testField = 'my-valid-field'; + + $this->assertTrue( + $this->customConditionProcessorBuilder->hasProcessorForField($testField) + ); + } + + public function testNegativeHasProcessorForField() + { + $testField = 'unknown-field'; + + $this->assertFalse( + $this->customConditionProcessorBuilder->hasProcessorForField($testField) + ); + } + + public function testPositiveGetProcessorByField() + { + $testField = 'my-valid-field'; + + $this->assertEquals( + $this->customConditionMock, + $this->customConditionProcessorBuilder->getProcessorByField($testField) + ); + } + + /** + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage Custom processor for field "unknown-field" is absent. + */ + public function testNegativeGetProcessorByFieldExceptionFieldIsAbsent() + { + $testField = 'unknown-field'; + $this->customConditionProcessorBuilder->getProcessorByField($testField); + } + + /** + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage Custom processor must implement "Magento\Framework\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\CustomConditionInterface". + */ + public function testNegativeGetProcessorByFieldExceptionWrongClass() + { + $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->customConditionProcessorBuilder = $objectManagerHelper + ->getObject( + CustomConditionProvider::class, + [ + 'customConditionProcessors' => [ + 'my-valid-field' => $this->customConditionMock, + 'my-invalid-field' => 'olo-lo' + ] + ] + ); + } +} From 6b2f7afd2943156dada3a2eb9938cadf7b028682 Mon Sep 17 00:00:00 2001 From: Andrii Voskoboinikov Date: Wed, 18 Apr 2018 15:54:30 +0300 Subject: [PATCH 2/3] MAGETWO-90564: [Indexer optimizations] Catalog Rule indexer matching mechanism optimization --- .../ConditionProcessor/ConditionBuilder/FactoryTest.php | 2 ++ .../Model/Rule/Condition/MappableConditionProcessorTest.php | 2 ++ .../ResourceModel/Product/ConditionsToCollectionApplierTest.php | 2 ++ .../_files/conditions_to_collection/attribute_sets.php | 1 + .../_files/conditions_to_collection/attribute_sets_rollback.php | 1 + .../CatalogRule/_files/conditions_to_collection/categories.php | 1 + .../_files/conditions_to_collection/categories_rollback.php | 1 + .../CatalogRule/_files/conditions_to_collection/products.php | 1 + .../_files/conditions_to_collection/products_rollback.php | 1 + .../ConditionProcessor/CustomConditionProviderTest.php | 2 ++ 10 files changed, 14 insertions(+) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/FactoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/FactoryTest.php index 70b75e7ff2790..e29eef0bed076 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/FactoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/FactoryTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Test\Unit\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\ConditionBuilder; use Magento\Eav\Model\Config as EavConfig; diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Rule/Condition/MappableConditionProcessorTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Rule/Condition/MappableConditionProcessorTest.php index 1643b3473ae27..e28c443e46fed 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/Rule/Condition/MappableConditionProcessorTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Rule/Condition/MappableConditionProcessorTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogRule\Test\Unit\Model\Rule\Condition; use Magento\Eav\Model\Config as EavConfig; diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplierTest.php b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplierTest.php index 75bdfc582068c..78fa255897dbc 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplierTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplierTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogRule\Model\ResourceModel\Product; use Magento\TestFramework\Helper\Bootstrap; diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/attribute_sets.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/attribute_sets.php index 45999a7279814..ed902833bd949 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/attribute_sets.php +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/attribute_sets.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $attributeSetFactory = $objectManager->get(\Magento\Eav\Api\Data\AttributeSetInterfaceFactory::class); diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/attribute_sets_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/attribute_sets_rollback.php index 23afac12b9fdd..9040ef15e0ccd 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/attribute_sets_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/attribute_sets_rollback.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var \Magento\Eav\Model\Entity\Attribute\Set $attributeSet */ diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/categories.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/categories.php index 485f2895b2f9b..fd10f32e5c594 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/categories.php +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/categories.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/categories_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/categories_rollback.php index 04d42692cea7a..a128ae05769e7 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/categories_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/categories_rollback.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products.php index 8b52f2604b531..db44a3d10ac9c 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products.php +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products_rollback.php index 0e591d20f8a6a..1ac4e4ec87385 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products_rollback.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); diff --git a/lib/internal/Magento/Framework/Api/Test/Unit/SearchCriteria/CollectionProcessor/ConditionProcessor/CustomConditionProviderTest.php b/lib/internal/Magento/Framework/Api/Test/Unit/SearchCriteria/CollectionProcessor/ConditionProcessor/CustomConditionProviderTest.php index 446eafb2cb414..3361a250ea730 100644 --- a/lib/internal/Magento/Framework/Api/Test/Unit/SearchCriteria/CollectionProcessor/ConditionProcessor/CustomConditionProviderTest.php +++ b/lib/internal/Magento/Framework/Api/Test/Unit/SearchCriteria/CollectionProcessor/ConditionProcessor/CustomConditionProviderTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\Api\Test\Unit\SearchCriteria\CollectionProcessor\ConditionProcessor; use Magento\Framework\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\CustomConditionProvider; From 48c1a7ae64c335d801e120c4f4d579dcb0c1c23d Mon Sep 17 00:00:00 2001 From: Andrii Voskoboinikov Date: Thu, 19 Apr 2018 14:21:45 +0300 Subject: [PATCH 3/3] MAGETWO-90564: [Indexer optimizations] Catalog Rule indexer matching mechanism optimization --- .../EavAttributeCondition.php | 2 + .../ConditionBuilder/Factory.php | 4 ++ .../NativeAttributeCondition.php | 3 ++ .../ConditionProcessor/DefaultCondition.php | 2 + .../ProductCategoryCondition.php | 10 +++-- app/code/Magento/CatalogRule/Model/Rule.php | 7 +++- .../ConditionsToSearchCriteriaMapper.php | 42 +++++++++---------- 7 files changed, 44 insertions(+), 26 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php index ca8b53ea0db00..d3c84e69c9540 100644 --- a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php @@ -42,6 +42,8 @@ public function __construct( } /** + * Build condition to filter product collection by EAV attribute + * * @param Filter $filter * @return string * @throws \DomainException diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/Factory.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/Factory.php index 808878d1481a9..66ddd0b8c0ad8 100644 --- a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/Factory.php +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/Factory.php @@ -57,6 +57,10 @@ public function __construct( } /** + * Decides which condition builder should be used for passed filter + * can be either EAV attribute builder or native attribute builder + * "native" attribute means attribute that is in catalog_product_entity table + * * @param Filter $filter * @return CustomConditionInterface * @throws \Magento\Framework\Exception\LocalizedException diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php index 976714dcd9aea..d072acf4c719c 100644 --- a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php @@ -34,6 +34,9 @@ public function __construct( } /** + * Build condition to filter product collection by product native attribute + * "native" attribute means attribute that is in catalog_product_entity table + * * @param Filter $filter * @return string * @throws \DomainException diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/DefaultCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/DefaultCondition.php index 5189da35ab52a..14685d87762c2 100644 --- a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/DefaultCondition.php +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/DefaultCondition.php @@ -31,6 +31,8 @@ public function __construct( } /** + * Builds condition to filter product collection either by EAV or by native attribute + * * @param Filter $filter * @return string */ diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php index 09c3a3864bbc7..f70bab73d0830 100644 --- a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php @@ -48,6 +48,8 @@ public function __construct( } /** + * Builds condition to filter product collection by categories + * * @param Filter $filter * @return string */ @@ -109,14 +111,14 @@ private function getCategoryIds(Filter $filter): array * Map equal and not equal conditions to in and not in * * @param string $conditionType - * @return mixed + * @return string */ private function mapConditionType(string $conditionType): string { $conditionsMap = [ - 'eq' => 'in', - 'neq' => 'nin', - 'like' => 'in', + 'eq' => 'in', + 'neq' => 'nin', + 'like' => 'in', 'nlike' => 'nin', ]; return $conditionsMap[$conditionType] ?? $conditionType; diff --git a/app/code/Magento/CatalogRule/Model/Rule.php b/app/code/Magento/CatalogRule/Model/Rule.php index 3d98de7fe6768..7696569cb26da 100644 --- a/app/code/Magento/CatalogRule/Model/Rule.php +++ b/app/code/Magento/CatalogRule/Model/Rule.php @@ -163,7 +163,7 @@ class Rule extends \Magento\Rule\Model\AbstractModel implements RuleInterface, I /** * @var ConditionsToCollectionApplier */ - protected $conditionsToCollectionApplier; + private $conditionsToCollectionApplier; /** * @var array @@ -372,6 +372,11 @@ public function getMatchingProductIds() return $this->_productIds; } + /** + * Check if we can use mapping for rule conditions + * + * @return bool + */ private function canPreMapProducts() { $conditions = $this->getConditions(); diff --git a/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php b/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php index 7dc832a28ac0e..6d343fe149d21 100644 --- a/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php +++ b/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php @@ -183,16 +183,16 @@ private function getGlueForArrayValues(string $operator): string private function reverseSqlOperatorInFilter(Filter $filter) { $operatorsMap = [ - 'eq' => 'neq', - 'neq' => 'eq', - 'gteq' => 'lt', - 'lteq' => 'gt', - 'gt' => 'lteq', - 'lt' => 'gteq', - 'like' => 'nlike', + 'eq' => 'neq', + 'neq' => 'eq', + 'gteq' => 'lt', + 'lteq' => 'gt', + 'gt' => 'lteq', + 'lt' => 'gteq', + 'like' => 'nlike', 'nlike' => 'like', - 'in' => 'nin', - 'nin' => 'in', + 'in' => 'nin', + 'nin' => 'in', ]; if (!array_key_exists($filter->getConditionType(), $operatorsMap)) { @@ -254,16 +254,16 @@ private function createFilter(string $field, string $value, string $conditionTyp private function mapRuleOperatorToSQLCondition(string $ruleOperator): string { $operatorsMap = [ - '==' => 'eq', // is - '!=' => 'neq', // is not - '>=' => 'gteq', // equals or greater than - '<=' => 'lteq', // equals or less than - '>' => 'gt', // greater than - '<' => 'lt', // less than - '{}' => 'like', // contains - '!{}' => 'nlike', // does not contains - '()' => 'in', // is one of - '!()' => 'nin', // is not one of + '==' => 'eq', // is + '!=' => 'neq', // is not + '>=' => 'gteq', // equals or greater than + '<=' => 'lteq', // equals or less than + '>' => 'gt', // greater than + '<' => 'lt', // less than + '{}' => 'like', // contains + '!{}' => 'nlike', // does not contains + '()' => 'in', // is one of + '!()' => 'nin', // is not one of ]; if (!array_key_exists($ruleOperator, $operatorsMap)) { @@ -289,8 +289,8 @@ private function mapRuleOperatorToSQLCondition(string $ruleOperator): string private function mapRuleAggregatorToSQLAggregator(string $ruleAggregator): string { $operatorsMap = [ - 'all' => 'AND', - 'any' => 'OR', + 'all' => 'AND', + 'any' => 'OR', ]; if (!array_key_exists(strtolower($ruleAggregator), $operatorsMap)) {