diff --git a/app/code/Magento/Backend/App/Request/BackendValidator.php b/app/code/Magento/Backend/App/Request/BackendValidator.php index 4d04d2fed8eb2..7ec6c83f29b50 100644 --- a/app/code/Magento/Backend/App/Request/BackendValidator.php +++ b/app/code/Magento/Backend/App/Request/BackendValidator.php @@ -146,8 +146,9 @@ private function createException( $exception = new InvalidRequestException($response); } else { //For regular requests. + $startPageUrl = $this->backendUrl->getStartupPageUrl(); $response = $this->redirectFactory->create() - ->setUrl($this->backendUrl->getStartupPageUrl()); + ->setUrl($this->backendUrl->getUrl($startPageUrl)); $exception = new InvalidRequestException( $response, [ diff --git a/app/code/Magento/Backend/Block/System/Design/Edit/Tab/General.php b/app/code/Magento/Backend/Block/System/Design/Edit/Tab/General.php index 5a09e1f17f617..6004d08b4b738 100644 --- a/app/code/Magento/Backend/Block/System/Design/Edit/Tab/General.php +++ b/app/code/Magento/Backend/Block/System/Design/Edit/Tab/General.php @@ -6,6 +6,9 @@ namespace Magento\Backend\Block\System\Design\Edit\Tab; +/** + * General system tab block. + */ class General extends \Magento\Backend\Block\Widget\Form\Generic { /** @@ -90,7 +93,7 @@ protected function _prepareForm() ] ); - $dateFormat = $this->_localeDate->getDateFormat(\IntlDateFormatter::SHORT); + $dateFormat = $this->_localeDate->getDateFormatWithLongYear(); $fieldset->addField( 'date_from', 'date', diff --git a/app/code/Magento/Catalog/Block/Product/ListProduct.php b/app/code/Magento/Catalog/Block/Product/ListProduct.php index c1b255c762dbb..c1d79894162ae 100644 --- a/app/code/Magento/Catalog/Block/Product/ListProduct.php +++ b/app/code/Magento/Catalog/Block/Product/ListProduct.php @@ -178,8 +178,9 @@ private function getDefaultListingMode() } /** - * Need use as _prepareLayout - but problem in declaring collection from - * another block (was problem with search result) + * Need use as _prepareLayout - but problem in declaring collection from another block. + * (was problem with search result) + * * @return $this */ protected function _beforeToHtml() @@ -188,7 +189,9 @@ protected function _beforeToHtml() $this->addToolbarBlock($collection); - $collection->load(); + if (!$collection->isLoaded()) { + $collection->load(); + } return parent::_beforeToHtml(); } @@ -262,6 +265,8 @@ public function getToolbarHtml() } /** + * Set collection. + * * @param AbstractCollection $collection * @return $this */ @@ -272,7 +277,9 @@ public function setCollection($collection) } /** - * @param array|string|integer| Element $code + * Add attribute. + * + * @param array|string|integer|Element $code * @return $this */ public function addAttribute($code) @@ -282,6 +289,8 @@ public function addAttribute($code) } /** + * Get price block template. + * * @return mixed */ public function getPriceBlockTemplate() @@ -371,6 +380,8 @@ public function getAddToCartPostParams(Product $product) } /** + * Get product price. + * * @param Product $product * @return string */ @@ -396,8 +407,8 @@ public function getProductPrice(Product $product) } /** - * Specifies that price rendering should be done for the list of products - * i.e. rendering happens in the scope of product list, but not single product + * Specifies that price rendering should be done for the list of products. + * (rendering happens in the scope of product list, but not single product) * * @return Render */ diff --git a/app/code/Magento/Catalog/Block/Product/View/Attributes.php b/app/code/Magento/Catalog/Block/Product/View/Attributes.php index 1cf9851d403a0..5b9777cbfd1e7 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Attributes.php +++ b/app/code/Magento/Catalog/Block/Product/View/Attributes.php @@ -94,7 +94,7 @@ public function getAdditionalData(array $excludeAttr = []) if (is_string($value) && strlen(trim($value))) { $data[$attribute->getAttributeCode()] = [ - 'label' => __($attribute->getStoreLabel()), + 'label' => $attribute->getStoreLabel(), 'value' => $value, 'code' => $attribute->getAttributeCode(), ]; diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php index 125406061aed7..78ad9f423871f 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\ProductFactory; use Magento\Cms\Model\Wysiwyg as WysiwygModel; use Magento\Framework\App\RequestInterface; @@ -15,6 +18,11 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Type as ProductTypes; +/** + * Build a product based on a request + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Builder { /** @@ -79,10 +87,11 @@ public function __construct( * Build product based on user request * * @param RequestInterface $request - * @return \Magento\Catalog\Model\Product + * @return ProductInterface * @throws \RuntimeException + * @throws \Magento\Framework\Exception\LocalizedException */ - public function build(RequestInterface $request) + public function build(RequestInterface $request): ProductInterface { $productId = (int) $request->getParam('id'); $storeId = $request->getParam('store', 0); @@ -92,6 +101,9 @@ public function build(RequestInterface $request) if ($productId) { try { $product = $this->productRepository->getById($productId, true, $storeId); + if ($attributeSetId) { + $product->setAttributeSetId($attributeSetId); + } } catch (\Exception $e) { $product = $this->createEmptyProduct(ProductTypes::DEFAULT_TYPE, $attributeSetId, $storeId); $this->logger->critical($e); @@ -113,6 +125,8 @@ public function build(RequestInterface $request) } /** + * Create a product with the given properties + * * @param int $typeId * @param int $attributeSetId * @param int $storeId diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php index f8121b55dbf99..eb59acb56c356 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php @@ -3,33 +3,46 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Catalog\Model\Indexer\Category\Product\Action; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Config; +use Magento\Catalog\Model\Indexer\Category\Product\AbstractAction; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Query\Generator as QueryGenerator; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Indexer\BatchProviderInterface; +use Magento\Framework\Indexer\BatchSizeManagementInterface; use Magento\Indexer\Model\ProcessManager; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; /** * Class Full reindex action * - * @package Magento\Catalog\Model\Indexer\Category\Product\Action * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractAction +class Full extends AbstractAction { /** - * @var \Magento\Framework\Indexer\BatchSizeManagementInterface + * @var BatchSizeManagementInterface */ private $batchSizeManagement; /** - * @var \Magento\Framework\Indexer\BatchProviderInterface + * @var BatchProviderInterface */ private $batchProvider; /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var MetadataPool */ protected $metadataPool; @@ -52,25 +65,25 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio /** * @param ResourceConnection $resource - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Catalog\Model\Config $config + * @param StoreManagerInterface $storeManager + * @param Config $config * @param QueryGenerator|null $queryGenerator - * @param \Magento\Framework\Indexer\BatchSizeManagementInterface|null $batchSizeManagement - * @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider - * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool + * @param BatchSizeManagementInterface|null $batchSizeManagement + * @param BatchProviderInterface|null $batchProvider + * @param MetadataPool|null $metadataPool * @param int|null $batchRowsCount * @param ActiveTableSwitcher|null $activeTableSwitcher * @param ProcessManager $processManager * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\App\ResourceConnection $resource, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Catalog\Model\Config $config, + ResourceConnection $resource, + StoreManagerInterface $storeManager, + Config $config, QueryGenerator $queryGenerator = null, - \Magento\Framework\Indexer\BatchSizeManagementInterface $batchSizeManagement = null, - \Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null, - \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, + BatchSizeManagementInterface $batchSizeManagement = null, + BatchProviderInterface $batchProvider = null, + MetadataPool $metadataPool = null, $batchRowsCount = null, ActiveTableSwitcher $activeTableSwitcher = null, ProcessManager $processManager = null @@ -81,15 +94,15 @@ public function __construct( $config, $queryGenerator ); - $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); + $objectManager = ObjectManager::getInstance(); $this->batchSizeManagement = $batchSizeManagement ?: $objectManager->get( - \Magento\Framework\Indexer\BatchSizeManagementInterface::class + BatchSizeManagementInterface::class ); $this->batchProvider = $batchProvider ?: $objectManager->get( - \Magento\Framework\Indexer\BatchProviderInterface::class + BatchProviderInterface::class ); $this->metadataPool = $metadataPool ?: $objectManager->get( - \Magento\Framework\EntityManager\MetadataPool::class + MetadataPool::class ); $this->batchRowsCount = $batchRowsCount; $this->activeTableSwitcher = $activeTableSwitcher ?: $objectManager->get(ActiveTableSwitcher::class); @@ -97,33 +110,39 @@ public function __construct( } /** + * Create the store tables + * * @return void */ - private function createTables() + private function createTables(): void { foreach ($this->storeManager->getStores() as $store) { - $this->tableMaintainer->createTablesForStore($store->getId()); + $this->tableMaintainer->createTablesForStore((int)$store->getId()); } } /** + * Truncates the replica tables + * * @return void */ - private function clearReplicaTables() + private function clearReplicaTables(): void { foreach ($this->storeManager->getStores() as $store) { - $this->connection->truncateTable($this->tableMaintainer->getMainReplicaTable($store->getId())); + $this->connection->truncateTable($this->tableMaintainer->getMainReplicaTable((int)$store->getId())); } } /** + * Switches the active table + * * @return void */ - private function switchTables() + private function switchTables(): void { $tablesToSwitch = []; foreach ($this->storeManager->getStores() as $store) { - $tablesToSwitch[] = $this->tableMaintainer->getMainTable($store->getId()); + $tablesToSwitch[] = $this->tableMaintainer->getMainTable((int)$store->getId()); } $this->activeTableSwitcher->switchTable($this->connection, $tablesToSwitch); } @@ -133,12 +152,13 @@ private function switchTables() * * @return $this */ - public function execute() + public function execute(): self { $this->createTables(); $this->clearReplicaTables(); $this->reindex(); $this->switchTables(); + return $this; } @@ -147,7 +167,7 @@ public function execute() * * @return void */ - protected function reindex() + protected function reindex(): void { $userFunctions = []; @@ -165,9 +185,9 @@ protected function reindex() /** * Execute indexation by store * - * @param \Magento\Store\Model\Store $store + * @param Store $store */ - private function reindexStore($store) + private function reindexStore($store): void { $this->reindexRootCategory($store); $this->reindexAnchorCategories($store); @@ -177,31 +197,31 @@ private function reindexStore($store) /** * Publish data from tmp to replica table * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - private function publishData($store) + private function publishData($store): void { - $select = $this->connection->select()->from($this->tableMaintainer->getMainTmpTable($store->getId())); + $select = $this->connection->select()->from($this->tableMaintainer->getMainTmpTable((int)$store->getId())); $columns = array_keys( - $this->connection->describeTable($this->tableMaintainer->getMainReplicaTable($store->getId())) + $this->connection->describeTable($this->tableMaintainer->getMainReplicaTable((int)$store->getId())) ); - $tableName = $this->tableMaintainer->getMainReplicaTable($store->getId()); + $tableName = $this->tableMaintainer->getMainReplicaTable((int)$store->getId()); $this->connection->query( $this->connection->insertFromSelect( $select, $tableName, $columns, - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ) ); } /** - * {@inheritdoc} + * @inheritdoc */ - protected function reindexRootCategory(\Magento\Store\Model\Store $store) + protected function reindexRootCategory(Store $store): void { if ($this->isIndexRootCategoryNeeded()) { $this->reindexCategoriesBySelect($this->getAllProducts($store), 'cp.entity_id IN (?)', $store); @@ -211,10 +231,10 @@ protected function reindexRootCategory(\Magento\Store\Model\Store $store) /** * Reindex products of anchor categories * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - protected function reindexAnchorCategories(\Magento\Store\Model\Store $store) + protected function reindexAnchorCategories(Store $store): void { $this->reindexCategoriesBySelect($this->getAnchorCategoriesSelect($store), 'ccp.product_id IN (?)', $store); } @@ -222,10 +242,10 @@ protected function reindexAnchorCategories(\Magento\Store\Model\Store $store) /** * Reindex products of non anchor categories * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - protected function reindexNonAnchorCategories(\Magento\Store\Model\Store $store) + protected function reindexNonAnchorCategories(Store $store): void { $this->reindexCategoriesBySelect($this->getNonAnchorCategoriesSelect($store), 'ccp.product_id IN (?)', $store); } @@ -233,40 +253,42 @@ protected function reindexNonAnchorCategories(\Magento\Store\Model\Store $store) /** * Reindex categories using given SQL select and condition. * - * @param \Magento\Framework\DB\Select $basicSelect + * @param Select $basicSelect * @param string $whereCondition - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - private function reindexCategoriesBySelect(\Magento\Framework\DB\Select $basicSelect, $whereCondition, $store) + private function reindexCategoriesBySelect(Select $basicSelect, $whereCondition, $store): void { - $this->tableMaintainer->createMainTmpTable($store->getId()); + $this->tableMaintainer->createMainTmpTable((int)$store->getId()); - $entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); + $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); $columns = array_keys( - $this->connection->describeTable($this->tableMaintainer->getMainTmpTable($store->getId())) + $this->connection->describeTable($this->tableMaintainer->getMainTmpTable((int)$store->getId())) ); $this->batchSizeManagement->ensureBatchSize($this->connection, $this->batchRowsCount); - $batches = $this->batchProvider->getBatches( - $this->connection, - $entityMetadata->getEntityTable(), + + $select = $this->connection->select(); + $select->distinct(true); + $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + + $batchQueries = $this->prepareSelectsByRange( + $select, $entityMetadata->getIdentifierField(), - $this->batchRowsCount + (int)$this->batchRowsCount ); - foreach ($batches as $batch) { - $this->connection->delete($this->tableMaintainer->getMainTmpTable($store->getId())); + + foreach ($batchQueries as $query) { + $this->connection->delete($this->tableMaintainer->getMainTmpTable((int)$store->getId())); + $entityIds = $this->connection->fetchCol($query); $resultSelect = clone $basicSelect; - $select = $this->connection->select(); - $select->distinct(true); - $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); - $entityIds = $this->batchProvider->getBatchIds($this->connection, $select, $batch); $resultSelect->where($whereCondition, $entityIds); $this->connection->query( $this->connection->insertFromSelect( $resultSelect, - $this->tableMaintainer->getMainTmpTable($store->getId()), + $this->tableMaintainer->getMainTmpTable((int)$store->getId()), $columns, - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ) ); $this->publishData($store); diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php index 802176092d147..ed8f692885d91 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php @@ -7,26 +7,41 @@ namespace Magento\Catalog\Model\Indexer\Product\Eav\Action; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Query\BatchIteratorInterface; +use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Indexer\BatchProviderInterface; +use Magento\Store\Model\ScopeInterface; /** * Class Full reindex action + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction +class Full extends AbstractAction { /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var MetadataPool */ private $metadataPool; /** - * @var \Magento\Framework\Indexer\BatchProviderInterface + * @var BatchProviderInterface */ private $batchProvider; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator + * @var BatchSizeCalculator */ private $batchSizeCalculator; @@ -36,44 +51,54 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction private $activeTableSwitcher; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var ScopeConfigInterface */ private $scopeConfig; /** - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory $eavDecimalFactory - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory $eavSourceFactory - * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool - * @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator $batchSizeCalculator + * @var QueryGenerator|null + */ + private $batchQueryGenerator; + + /** + * @param DecimalFactory $eavDecimalFactory + * @param SourceFactory $eavSourceFactory + * @param MetadataPool|null $metadataPool + * @param BatchProviderInterface|null $batchProvider + * @param BatchSizeCalculator $batchSizeCalculator * @param ActiveTableSwitcher|null $activeTableSwitcher - * @param \Magento\Framework\App\Config\ScopeConfigInterface|null $scopeConfig + * @param ScopeConfigInterface|null $scopeConfig + * @param QueryGenerator|null $batchQueryGenerator */ public function __construct( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory $eavDecimalFactory, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory $eavSourceFactory, - \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, - \Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator $batchSizeCalculator = null, + DecimalFactory $eavDecimalFactory, + SourceFactory $eavSourceFactory, + MetadataPool $metadataPool = null, + BatchProviderInterface $batchProvider = null, + BatchSizeCalculator $batchSizeCalculator = null, ActiveTableSwitcher $activeTableSwitcher = null, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig = null + ScopeConfigInterface $scopeConfig = null, + QueryGenerator $batchQueryGenerator = null ) { - $this->scopeConfig = $scopeConfig ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\App\Config\ScopeConfigInterface::class + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get( + ScopeConfigInterface::class ); parent::__construct($eavDecimalFactory, $eavSourceFactory, $scopeConfig); - $this->metadataPool = $metadataPool ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\EntityManager\MetadataPool::class + $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get( + MetadataPool::class ); - $this->batchProvider = $batchProvider ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\Indexer\BatchProviderInterface::class + $this->batchProvider = $batchProvider ?: ObjectManager::getInstance()->get( + BatchProviderInterface::class ); - $this->batchSizeCalculator = $batchSizeCalculator ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator::class + $this->batchSizeCalculator = $batchSizeCalculator ?: ObjectManager::getInstance()->get( + BatchSizeCalculator::class ); - $this->activeTableSwitcher = $activeTableSwitcher ?: \Magento\Framework\App\ObjectManager::getInstance()->get( + $this->activeTableSwitcher = $activeTableSwitcher ?: ObjectManager::getInstance()->get( ActiveTableSwitcher::class ); + $this->batchQueryGenerator = $batchQueryGenerator ?: ObjectManager::getInstance()->get( + QueryGenerator::class + ); } /** @@ -81,10 +106,10 @@ public function __construct( * * @param array|int|null $ids * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function execute($ids = null) + public function execute($ids = null): void { if (!$this->isEavIndexerEnabled()) { return; @@ -94,20 +119,21 @@ public function execute($ids = null) $connection = $indexer->getConnection(); $mainTable = $this->activeTableSwitcher->getAdditionalTableName($indexer->getMainTable()); $connection->truncateTable($mainTable); - $entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); - $batches = $this->batchProvider->getBatches( - $connection, - $entityMetadata->getEntityTable(), + $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); + + $select = $connection->select(); + $select->distinct(true); + $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + + $batchQueries = $this->batchQueryGenerator->generate( $entityMetadata->getIdentifierField(), - $this->batchSizeCalculator->estimateBatchSize($connection, $indexerName) + $select, + $this->batchSizeCalculator->estimateBatchSize($connection, $indexerName), + BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR ); - foreach ($batches as $batch) { - /** @var \Magento\Framework\DB\Select $select */ - $select = $connection->select(); - $select->distinct(true); - $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); - $entityIds = $this->batchProvider->getBatchIds($connection, $select, $batch); + foreach ($batchQueries as $query) { + $entityIds = $connection->fetchCol($query); if (!empty($entityIds)) { $indexer->reindexEntities($this->processRelations($indexer, $entityIds, true)); $this->syncData($indexer, $mainTable); @@ -116,14 +142,14 @@ public function execute($ids = null) $this->activeTableSwitcher->switchTable($indexer->getConnection(), [$indexer->getMainTable()]); } } catch (\Exception $e) { - throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage()), $e); + throw new LocalizedException(__($e->getMessage()), $e); } } /** * @inheritdoc */ - protected function syncData($indexer, $destinationTable, $ids = null) + protected function syncData($indexer, $destinationTable, $ids = null): void { $connection = $indexer->getConnection(); $connection->beginTransaction(); @@ -136,7 +162,7 @@ protected function syncData($indexer, $destinationTable, $ids = null) $select, $destinationTable, $targetColumns, - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ); $connection->query($query); $connection->commit(); @@ -155,7 +181,7 @@ private function isEavIndexerEnabled(): bool { $eavIndexerStatus = $this->scopeConfig->getValue( self::ENABLE_EAV_INDEXER, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); return (bool)$eavIndexerStatus; diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php index 1a75751570658..858eba3ab217a 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php @@ -3,41 +3,64 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Catalog\Model\Indexer\Product\Price\Action; +use Magento\Catalog\Model\Indexer\Product\Price\AbstractAction; +use Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory; +use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Query\BatchIterator; +use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\EntityMetadataInterface; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Indexer\BatchProviderInterface; use Magento\Framework\Indexer\DimensionalIndexerInterface; use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; +use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Indexer\Model\ProcessManager; use Magento\Store\Model\Indexer\WebsiteDimensionProvider; +use Magento\Store\Model\StoreManagerInterface; /** * Class Full reindex action * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction +class Full extends AbstractAction { /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var MetadataPool */ private $metadataPool; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator + * @var BatchSizeCalculator */ private $batchSizeCalculator; /** - * @var \Magento\Framework\Indexer\BatchProviderInterface + * @var BatchProviderInterface */ private $batchProvider; /** - * @var \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher + * @var ActiveTableSwitcher */ private $activeTableSwitcher; @@ -47,54 +70,61 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction private $productMetaDataCached; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory + * @var DimensionCollectionFactory */ private $dimensionCollectionFactory; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer + * @var TableMaintainer */ private $dimensionTableMaintainer; /** - * @var \Magento\Indexer\Model\ProcessManager + * @var ProcessManager */ private $processManager; /** - * @param \Magento\Framework\App\Config\ScopeConfigInterface $config - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Directory\Model\CurrencyFactory $currencyFactory - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate - * @param \Magento\Framework\Stdlib\DateTime $dateTime - * @param \Magento\Catalog\Model\Product\Type $catalogProductType - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory $indexerPriceFactory - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $defaultIndexerResource - * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator|null $batchSizeCalculator - * @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider - * @param \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher|null $activeTableSwitcher - * @param \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory|null $dimensionCollectionFactory - * @param \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer|null $dimensionTableMaintainer - * @param \Magento\Indexer\Model\ProcessManager $processManager + * @var QueryGenerator|null + */ + private $batchQueryGenerator; + + /** + * @param ScopeConfigInterface $config + * @param StoreManagerInterface $storeManager + * @param CurrencyFactory $currencyFactory + * @param TimezoneInterface $localeDate + * @param DateTime $dateTime + * @param Type $catalogProductType + * @param Factory $indexerPriceFactory + * @param DefaultPrice $defaultIndexerResource + * @param MetadataPool|null $metadataPool + * @param BatchSizeCalculator|null $batchSizeCalculator + * @param BatchProviderInterface|null $batchProvider + * @param ActiveTableSwitcher|null $activeTableSwitcher + * @param DimensionCollectionFactory|null $dimensionCollectionFactory + * @param TableMaintainer|null $dimensionTableMaintainer + * @param ProcessManager $processManager + * @param QueryGenerator|null $batchQueryGenerator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\App\Config\ScopeConfigInterface $config, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Directory\Model\CurrencyFactory $currencyFactory, - \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, - \Magento\Framework\Stdlib\DateTime $dateTime, - \Magento\Catalog\Model\Product\Type $catalogProductType, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory $indexerPriceFactory, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $defaultIndexerResource, - \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator $batchSizeCalculator = null, - \Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null, - \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher = null, - \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory $dimensionCollectionFactory = null, - \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer $dimensionTableMaintainer = null, - \Magento\Indexer\Model\ProcessManager $processManager = null + ScopeConfigInterface $config, + StoreManagerInterface $storeManager, + CurrencyFactory $currencyFactory, + TimezoneInterface $localeDate, + DateTime $dateTime, + Type $catalogProductType, + Factory $indexerPriceFactory, + DefaultPrice $defaultIndexerResource, + MetadataPool $metadataPool = null, + BatchSizeCalculator $batchSizeCalculator = null, + BatchProviderInterface $batchProvider = null, + ActiveTableSwitcher $activeTableSwitcher = null, + DimensionCollectionFactory $dimensionCollectionFactory = null, + TableMaintainer $dimensionTableMaintainer = null, + ProcessManager $processManager = null, + QueryGenerator $batchQueryGenerator = null ) { parent::__construct( $config, @@ -107,26 +137,27 @@ public function __construct( $defaultIndexerResource ); $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get( - \Magento\Framework\EntityManager\MetadataPool::class + MetadataPool::class ); $this->batchSizeCalculator = $batchSizeCalculator ?: ObjectManager::getInstance()->get( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator::class + BatchSizeCalculator::class ); $this->batchProvider = $batchProvider ?: ObjectManager::getInstance()->get( - \Magento\Framework\Indexer\BatchProviderInterface::class + BatchProviderInterface::class ); $this->activeTableSwitcher = $activeTableSwitcher ?: ObjectManager::getInstance()->get( - \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher::class + ActiveTableSwitcher::class ); $this->dimensionCollectionFactory = $dimensionCollectionFactory ?: ObjectManager::getInstance()->get( - \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory::class + DimensionCollectionFactory::class ); $this->dimensionTableMaintainer = $dimensionTableMaintainer ?: ObjectManager::getInstance()->get( - \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer::class + TableMaintainer::class ); $this->processManager = $processManager ?: ObjectManager::getInstance()->get( - \Magento\Indexer\Model\ProcessManager::class + ProcessManager::class ); + $this->batchQueryGenerator = $batchQueryGenerator ?? ObjectManager::getInstance()->get(QueryGenerator::class); } /** @@ -137,13 +168,13 @@ public function __construct( * @throws \Exception * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function execute($ids = null) + public function execute($ids = null): void { try { //Prepare indexer tables before full reindex $this->prepareTables(); - /** @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $indexer */ + /** @var DefaultPrice $indexer */ foreach ($this->getTypeIndexers(true) as $typeId => $priceIndexer) { if ($priceIndexer instanceof DimensionalIndexerInterface) { //New price reindex mechanism @@ -170,7 +201,7 @@ public function execute($ids = null) * @return void * @throws \Exception */ - private function prepareTables() + private function prepareTables(): void { $this->_defaultIndexerResource->getTableStrategy()->setUseIdxTable(false); @@ -185,7 +216,7 @@ private function prepareTables() * @return void * @throws \Exception */ - private function truncateReplicaTables() + private function truncateReplicaTables(): void { foreach ($this->dimensionCollectionFactory->create() as $dimension) { $dimensionTable = $this->dimensionTableMaintainer->getMainReplicaTable($dimension); @@ -202,12 +233,12 @@ private function truncateReplicaTables() * @return void * @throws \Exception */ - private function reindexProductTypeWithDimensions(DimensionalIndexerInterface $priceIndexer, string $typeId) + private function reindexProductTypeWithDimensions(DimensionalIndexerInterface $priceIndexer, string $typeId): void { $userFunctions = []; foreach ($this->dimensionCollectionFactory->create() as $dimensions) { $userFunctions[] = function () use ($priceIndexer, $dimensions, $typeId) { - return $this->reindexByBatches($priceIndexer, $dimensions, $typeId); + $this->reindexByBatches($priceIndexer, $dimensions, $typeId); }; } $this->processManager->execute($userFunctions); @@ -223,10 +254,13 @@ private function reindexProductTypeWithDimensions(DimensionalIndexerInterface $p * @return void * @throws \Exception */ - private function reindexByBatches(DimensionalIndexerInterface $priceIndexer, array $dimensions, string $typeId) - { + private function reindexByBatches( + DimensionalIndexerInterface $priceIndexer, + array $dimensions, + string $typeId + ): void { foreach ($this->getBatchesForIndexer($typeId) as $batch) { - $this->reindexByBatchWithDimensions($priceIndexer, $batch, $dimensions, $typeId); + $this->reindexByBatchWithDimensions($priceIndexer, $batch, $dimensions); } } @@ -235,16 +269,20 @@ private function reindexByBatches(DimensionalIndexerInterface $priceIndexer, arr * * @param string $typeId * - * @return \Generator + * @return BatchIterator * @throws \Exception */ - private function getBatchesForIndexer(string $typeId) + private function getBatchesForIndexer(string $typeId): BatchIterator { $connection = $this->_defaultIndexerResource->getConnection(); - return $this->batchProvider->getBatches( - $connection, - $this->getProductMetaData()->getEntityTable(), + $entityMetadata = $this->getProductMetaData(); + $select = $connection->select(); + $select->distinct(true); + $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + + return $this->batchQueryGenerator->generate( $this->getProductMetaData()->getIdentifierField(), + $select, $this->batchSizeCalculator->estimateBatchSize( $connection, $typeId @@ -256,20 +294,18 @@ private function getBatchesForIndexer(string $typeId) * Reindex by batch for new 'Dimensional' price indexer * * @param DimensionalIndexerInterface $priceIndexer - * @param array $batch + * @param Select $batchQuery * @param array $dimensions - * @param string $typeId * * @return void * @throws \Exception */ private function reindexByBatchWithDimensions( DimensionalIndexerInterface $priceIndexer, - array $batch, - array $dimensions, - string $typeId - ) { - $entityIds = $this->getEntityIdsFromBatch($typeId, $batch); + Select $batchQuery, + array $dimensions + ): void { + $entityIds = $this->getEntityIdsFromBatch($batchQuery); if (!empty($entityIds)) { $this->dimensionTableMaintainer->createMainTmpTable($dimensions); @@ -295,10 +331,10 @@ private function reindexByBatchWithDimensions( * @return void * @throws \Exception */ - private function reindexProductType(PriceInterface $priceIndexer, string $typeId) + private function reindexProductType(PriceInterface $priceIndexer, string $typeId): void { foreach ($this->getBatchesForIndexer($typeId) as $batch) { - $this->reindexBatch($priceIndexer, $batch, $typeId); + $this->reindexBatch($priceIndexer, $batch); } } @@ -306,15 +342,13 @@ private function reindexProductType(PriceInterface $priceIndexer, string $typeId * Reindex by batch for old price indexer * * @param PriceInterface $priceIndexer - * @param array $batch - * @param string $typeId - * + * @param Select $batch * @return void * @throws \Exception */ - private function reindexBatch(PriceInterface $priceIndexer, array $batch, string $typeId) + private function reindexBatch(PriceInterface $priceIndexer, Select $batch): void { - $entityIds = $this->getEntityIdsFromBatch($typeId, $batch); + $entityIds = $this->getEntityIdsFromBatch($batch); if (!empty($entityIds)) { // Temporary table will created if not exists @@ -339,27 +373,15 @@ private function reindexBatch(PriceInterface $priceIndexer, array $batch, string /** * Get Entity Ids from batch * - * @param string $typeId - * @param array $batch - * + * @param Select $batch * @return array * @throws \Exception */ - private function getEntityIdsFromBatch(string $typeId, array $batch) + private function getEntityIdsFromBatch(Select $batch): array { $connection = $this->_defaultIndexerResource->getConnection(); - // Get entity ids from batch - $select = $connection - ->select() - ->distinct(true) - ->from( - ['e' => $this->getProductMetaData()->getEntityTable()], - $this->getProductMetaData()->getIdentifierField() - ) - ->where('type_id = ?', $typeId); - - return $this->batchProvider->getBatchIds($connection, $select, $batch); + return $connection->fetchCol($batch); } /** @@ -368,7 +390,7 @@ private function getEntityIdsFromBatch(string $typeId, array $batch) * @return EntityMetadataInterface * @throws \Exception */ - private function getProductMetaData() + private function getProductMetaData(): EntityMetadataInterface { if ($this->productMetaDataCached === null) { $this->productMetaDataCached = $this->metadataPool->getMetadata(ProductInterface::class); @@ -383,7 +405,7 @@ private function getProductMetaData() * @return string * @throws \Exception */ - private function getReplicaTable() + private function getReplicaTable(): string { return $this->activeTableSwitcher->getAdditionalTableName( $this->_defaultIndexerResource->getMainTable() @@ -394,8 +416,9 @@ private function getReplicaTable() * Replacement of tables from replica to main * * @return void + * @throws \Zend_Db_Statement_Exception */ - private function switchTables() + private function switchTables(): void { // Switch dimension tables $mainTablesByDimension = []; @@ -417,13 +440,14 @@ private function switchTables() /** * Move data from old price indexer mechanism to new indexer mechanism by dimensions. + * * Used only for backward compatibility * * @param array $dimensions - * * @return void + * @throws \Zend_Db_Statement_Exception */ - private function moveDataFromReplicaTableToReplicaTables(array $dimensions) + private function moveDataFromReplicaTableToReplicaTables(array $dimensions): void { if (!$dimensions) { return; @@ -455,17 +479,17 @@ private function moveDataFromReplicaTableToReplicaTables(array $dimensions) $select, $replicaTablesByDimension, [], - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ) ); } /** - * @deprecated + * Retrieves the index table that should be used * - * @inheritdoc + * @deprecated */ - protected function getIndexTargetTable() + protected function getIndexTargetTable(): string { return $this->activeTableSwitcher->getAdditionalTableName($this->_defaultIndexerResource->getMainTable()); } diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSet.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSet.xml new file mode 100644 index 0000000000000..3027416ee520b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSet.xml @@ -0,0 +1,66 @@ + + + + + + + + + <description value="Attributes from the selected attribute set should be shown"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-98452"/> + <useCaseId value="MAGETWO-98357"/> + <group value="catalog"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <createData entity="productAttributeWithTwoOptions" stepKey="createProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createProductAttributeOption1"> + <requiredEntity createDataKey="createProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createProductAttributeOption2"> + <requiredEntity createDataKey="createProductAttribute"/> + </createData> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <amOnPage url="{{AdminProductAttributeSetEditPage.url}}/$$createAttributeSet.attribute_set_id$$}}/" stepKey="onAttributeSetEdit"/> + <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$$createProductAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="SaveAttributeSet" stepKey="SaveAttributeSet"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> + <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/> + </after> + + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct1"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <click selector="{{AdminProductFormSection.attributeSet}}" stepKey="startEditAttrSet"/> + <fillField selector="{{AdminProductFormSection.attributeSetFilter}}" userInput="$$createAttributeSet.attribute_set_name$$" stepKey="searchForAttrSet"/> + <click selector="{{AdminProductFormSection.attributeSetFilterResult}}" stepKey="selectAttrSet"/> + + <waitForText userInput="$$createProductAttribute.default_frontend_label$$" stepKey="seeAttributeInForm"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php index 90c3f999a6a8b..2e1cff834fd34 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php @@ -3,15 +3,29 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Eav\Action; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Indexer\Product\Eav\Action\Full; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Decimal; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Source; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Query\Generator; +use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Indexer\BatchProviderInterface; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator; +use PHPUnit\Framework\MockObject\MockObject as MockObject; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -19,45 +33,50 @@ class FullTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Catalog\Model\Indexer\Product\Eav\Action\Full|\PHPUnit_Framework_MockObject_MockObject + * @var Full|MockObject */ private $model; /** - * @var DecimalFactory|\PHPUnit_Framework_MockObject_MockObject + * @var DecimalFactory|MockObject */ private $eavDecimalFactory; /** - * @var SourceFactory|\PHPUnit_Framework_MockObject_MockObject + * @var SourceFactory|MockObject */ private $eavSourceFactory; /** - * @var MetadataPool|\PHPUnit_Framework_MockObject_MockObject + * @var MetadataPool|MockObject */ private $metadataPool; /** - * @var BatchProviderInterface|\PHPUnit_Framework_MockObject_MockObject + * @var BatchProviderInterface|MockObject */ private $batchProvider; /** - * @var BatchSizeCalculator|\PHPUnit_Framework_MockObject_MockObject + * @var BatchSizeCalculator|MockObject */ private $batchSizeCalculator; /** - * @var ActiveTableSwitcher|\PHPUnit_Framework_MockObject_MockObject + * @var ActiveTableSwitcher|MockObject */ private $activeTableSwitcher; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ScopeConfigInterface|MockObject */ private $scopeConfig; + /** + * @var Generator + */ + private $batchQueryGenerator; + /** * @return void */ @@ -67,15 +86,16 @@ protected function setUp() $this->eavSourceFactory = $this->createPartialMock(SourceFactory::class, ['create']); $this->metadataPool = $this->createMock(MetadataPool::class); $this->batchProvider = $this->getMockForAbstractClass(BatchProviderInterface::class); + $this->batchQueryGenerator = $this->createMock(Generator::class); $this->batchSizeCalculator = $this->createMock(BatchSizeCalculator::class); $this->activeTableSwitcher = $this->createMock(ActiveTableSwitcher::class); - $this->scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); $objectManager = new ObjectManager($this); $this->model = $objectManager->getObject( - \Magento\Catalog\Model\Indexer\Product\Eav\Action\Full::class, + Full::class, [ 'eavDecimalFactory' => $this->eavDecimalFactory, 'eavSourceFactory' => $this->eavSourceFactory, @@ -83,7 +103,8 @@ protected function setUp() 'batchProvider' => $this->batchProvider, 'batchSizeCalculator' => $this->batchSizeCalculator, 'activeTableSwitcher' => $this->activeTableSwitcher, - 'scopeConfig' => $this->scopeConfig + 'scopeConfig' => $this->scopeConfig, + 'batchQueryGenerator' => $this->batchQueryGenerator, ] ); } @@ -96,15 +117,15 @@ public function testExecute() $this->scopeConfig->expects($this->once())->method('getValue')->willReturn(1); $ids = [1, 2, 3]; - $connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) + $connectionMock = $this->getMockBuilder(AdapterInterface::class) ->getMockForAbstractClass(); $connectionMock->expects($this->atLeastOnce())->method('describeTable')->willReturn(['id' => []]); - $eavSource = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Source::class) + $eavSource = $this->getMockBuilder(Source::class) ->disableOriginalConstructor() ->getMock(); - $eavDecimal = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Decimal::class) + $eavDecimal = $this->getMockBuilder(Decimal::class) ->disableOriginalConstructor() ->getMock(); @@ -125,22 +146,28 @@ public function testExecute() $this->eavSourceFactory->expects($this->once())->method('create')->will($this->returnValue($eavDecimal)); - $entityMetadataMock = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) + $entityMetadataMock = $this->getMockBuilder(EntityMetadataInterface::class) ->getMockForAbstractClass(); $this->metadataPool->expects($this->atLeastOnce()) ->method('getMetadata') - ->with(\Magento\Catalog\Api\Data\ProductInterface::class) + ->with(ProductInterface::class) ->willReturn($entityMetadataMock); - $this->batchProvider->expects($this->atLeastOnce()) - ->method('getBatches') - ->willReturn([['from' => 10, 'to' => 100]]); - $this->batchProvider->expects($this->atLeastOnce()) - ->method('getBatchIds') + // Super inefficient algorithm in some cases + $this->batchProvider->expects($this->never()) + ->method('getBatches'); + + $batchQuery = $this->createMock(Select::class); + + $connectionMock->method('fetchCol') + ->with($batchQuery) ->willReturn($ids); - $selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) + $this->batchQueryGenerator->method('generate') + ->willReturn([$batchQuery]); + + $selectMock = $this->getMockBuilder(Select::class) ->disableOriginalConstructor() ->getMock(); @@ -153,7 +180,7 @@ public function testExecute() /** * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function testExecuteWithDisabledEavIndexer() { diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php new file mode 100644 index 0000000000000..774edcfeb6b64 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php @@ -0,0 +1,156 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Ui\Component; + +use PHPUnit\Framework\TestCase; +use Magento\Catalog\Ui\Component\ColumnFactory; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Listing\Columns\ColumnInterface; +use Magento\Ui\Component\Filters\FilterModifier; + +/** + * ColumnFactory test. + */ +class ColumnFactoryTest extends TestCase +{ + /** + * @var ColumnFactory + */ + private $columnFactory; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ProductAttributeInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private $attribute; + + /** + * @var ContextInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private $context; + + /** + * @var UiComponentFactory|\PHPUnit\Framework\MockObject\MockObject + */ + private $uiComponentFactory; + + /** + * @var ColumnInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private $column; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = new ObjectManager($this); + + $this->attribute = $this->getMockBuilder(ProductAttributeInterface::class) + ->setMethods(['usesSource']) + ->getMockForAbstractClass(); + $this->context = $this->createMock(ContextInterface::class); + $this->uiComponentFactory = $this->createMock(UiComponentFactory::class); + $this->column = $this->getMockForAbstractClass(ColumnInterface::class); + $this->uiComponentFactory->method('create') + ->willReturn($this->column); + + $this->columnFactory = $this->objectManager->getObject(ColumnFactory::class, [ + 'componentFactory' => $this->uiComponentFactory + ]); + } + + /** + * Tests the create method will return correct object. + * + * @return void + */ + public function testCreatedObject(): void + { + $this->context->method('getRequestParam') + ->with(FilterModifier::FILTER_MODIFIER, []) + ->willReturn([]); + + $object = $this->columnFactory->create($this->attribute, $this->context); + $this->assertEquals( + $this->column, + $object, + 'Object must be the same which the ui component factory creates.' + ); + } + + /** + * Tests create method with not filterable in grid attribute. + * + * @param array $filterModifiers + * @param null|string $filter + * + * @return void + * @dataProvider filterModifiersProvider + */ + public function testCreateWithNotFilterableInGridAttribute(array $filterModifiers, ?string $filter): void + { + $componentFactoryArgument = [ + 'data' => [ + 'config' => [ + 'label' => __(null), + 'dataType' => 'text', + 'add_field' => true, + 'visible' => null, + 'filter' => $filter, + 'component' => 'Magento_Ui/js/grid/columns/column', + ], + ], + 'context' => $this->context, + ]; + + $this->context->method('getRequestParam') + ->with(FilterModifier::FILTER_MODIFIER, []) + ->willReturn($filterModifiers); + $this->attribute->method('getIsFilterableInGrid') + ->willReturn(false); + $this->attribute->method('getAttributeCode') + ->willReturn('color'); + + $this->uiComponentFactory->expects($this->once()) + ->method('create') + ->with($this->anything(), $this->anything(), $componentFactoryArgument); + + $this->columnFactory->create($this->attribute, $this->context); + } + + /** + * Filter modifiers data provider. + * + * @return array + */ + public function filterModifiersProvider(): array + { + return [ + 'without' => [ + 'filter_modifiers' => [], + 'filter' => null, + ], + 'with' => [ + 'filter_modifiers' => [ + 'color' => [ + 'condition_type' => 'notnull', + ], + ], + 'filter' => 'text', + ], + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php index 6d7c8814bd474..0e0cb676cdf3e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php @@ -54,7 +54,16 @@ protected function setUp() ->getMockForAbstractClass(); $this->collectionMock = $this->getMockBuilder(AbstractCollection::class) ->disableOriginalConstructor() - ->setMethods(['load', 'getSelect', 'getTable', 'getIterator', 'isLoaded', 'toArray', 'getSize']) + ->setMethods([ + 'load', + 'getSelect', + 'getTable', + 'getIterator', + 'isLoaded', + 'toArray', + 'getSize', + 'setStoreId' + ]) ->getMockForAbstractClass(); $this->dbSelectMock = $this->getMockBuilder(DbSelect::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php b/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php index 1903bcd144831..ea6b1fd47a0a5 100644 --- a/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php +++ b/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php @@ -5,6 +5,8 @@ */ namespace Magento\Catalog\Ui\Component; +use Magento\Ui\Component\Filters\FilterModifier; + /** * Column Factory * @@ -60,13 +62,15 @@ public function __construct(\Magento\Framework\View\Element\UiComponentFactory $ */ public function create($attribute, $context, array $config = []) { + $filterModifiers = $context->getRequestParam(FilterModifier::FILTER_MODIFIER, []); + $columnName = $attribute->getAttributeCode(); $config = array_merge([ 'label' => __($attribute->getDefaultFrontendLabel()), 'dataType' => $this->getDataType($attribute), 'add_field' => true, 'visible' => $attribute->getIsVisibleInGrid(), - 'filter' => ($attribute->getIsFilterableInGrid()) + 'filter' => ($attribute->getIsFilterableInGrid() || array_key_exists($columnName, $filterModifiers)) ? $this->getFilterType($attribute->getFrontendInput()) : null, ], $config); diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php index 2a4d2ff52d479..00132c6ad89e8 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php @@ -389,6 +389,9 @@ private function addAdvancedPriceLink() 'componentType' => Container::NAME, 'component' => 'Magento_Ui/js/form/components/button', 'template' => 'ui/form/components/button/container', + 'imports' => [ + 'childError' => $this->scopeName . '.advanced_pricing_modal.advanced-pricing:error', + ], 'actions' => [ [ 'targetName' => $this->scopeName . '.advanced_pricing_modal', diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php index 200ecf89641fa..a518afc576d61 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php @@ -7,6 +7,7 @@ use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\Framework\App\ObjectManager; +use Magento\Store\Model\Store; use Magento\Ui\DataProvider\Modifier\ModifierInterface; use Magento\Ui\DataProvider\Modifier\PoolInterface; @@ -67,6 +68,7 @@ public function __construct( $this->addFieldStrategies = $addFieldStrategies; $this->addFilterStrategies = $addFilterStrategies; $this->modifiersPool = $modifiersPool ?: ObjectManager::getInstance()->get(PoolInterface::class); + $this->collection->setStoreId(Store::DEFAULT_STORE_ID); } /** @@ -110,7 +112,7 @@ public function addField($field, $alias = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function addFilter(\Magento\Framework\Api\Filter $filter) { diff --git a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml index 7e18ac34f0fcc..b5622e948b7cd 100644 --- a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml @@ -38,11 +38,15 @@ </item> <item name="customizable_options" xsi:type="array"> <item name="field" xsi:type="string">CustomizableFieldOption</item> + <item name="date" xsi:type="string">CustomizableDateOption</item> <item name="date_time" xsi:type="string">CustomizableDateOption</item> + <item name="time" xsi:type="string">CustomizableDateOption</item> <item name="file" xsi:type="string">CustomizableFileOption</item> <item name="area" xsi:type="string">CustomizableAreaOption</item> <item name="drop_down" xsi:type="string">CustomizableDropDownOption</item> + <item name="multiple" xsi:type="string">CustomizableMultipleOption</item> <item name="radio" xsi:type="string">CustomizableRadioOption</item> + <item name="checkbox" xsi:type="string">CustomizableCheckboxOption</item> </item> </argument> </arguments> diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 45f3a4c83be7b..778d0783305b7 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -323,6 +323,19 @@ type CustomizableDropDownValue @doc(description: "CustomizableDropDownValue defi sort_order: Int @doc(description: "The order in which the option is displayed") } +type CustomizableMultipleOption implements CustomizableOptionInterface @doc(description: "CustomizableMultipleOption contains information about a multiselect that is defined as part of a customizable option") { + value: [CustomizableMultipleValue] @doc(description: "An array that defines the set of options for a multiselect") +} + +type CustomizableMultipleValue @doc(description: "CustomizableMultipleValue defines the price and sku of a product whose page contains a customized multiselect") { + option_type_id: Int @doc(description: "The ID assigned to the value") + price: Float @doc(description: "The price assigned to this option") + price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC") + sku: String @doc(description: "The Stock Keeping Unit for this option") + title: String @doc(description: "The display name for this option") + sort_order: Int @doc(description: "The order in which the option is displayed") +} + type CustomizableFieldOption implements CustomizableOptionInterface @doc(description: "CustomizableFieldOption contains information about a text field that is defined as part of a customizable option") { value: CustomizableFieldValue @doc(description: "An object that defines a text field") product_sku: String @doc(description: "The Stock Keeping Unit of the base product") @@ -407,6 +420,19 @@ type CustomizableRadioValue @doc(description: "CustomizableRadioValue defines t sort_order: Int @doc(description: "The order in which the radio button is displayed") } +type CustomizableCheckboxOption implements CustomizableOptionInterface @doc(description: "CustomizableCheckbbixOption contains information about a set of checkbox values that are defined as part of a customizable option") { + value: [CustomizableCheckboxValue] @doc(description: "An array that defines a set of checkbox values") +} + +type CustomizableCheckboxValue @doc(description: "CustomizableCheckboxValue defines the price and sku of a product whose page contains a customized set of checkbox values") { + option_type_id: Int @doc(description: "The ID assigned to the value") + price: Float @doc(description: "The price assigned to this option") + price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC") + sku: String @doc(description: "The Stock Keeping Unit for this option") + title: String @doc(description: "The display name for this option") + sort_order: Int @doc(description: "The order in which the checkbox value is displayed") +} + type VirtualProduct implements ProductInterface, CustomizableProductInterface @doc(description: "A virtual product is non-tangible product that does not require shipping and is not kept in inventory") { } diff --git a/app/code/Magento/CatalogInventory/Model/AddStockStatusToCollection.php b/app/code/Magento/CatalogInventory/Model/AddStockStatusToCollection.php index 2c52b49c1a039..0a02d4eb6a9a6 100644 --- a/app/code/Magento/CatalogInventory/Model/AddStockStatusToCollection.php +++ b/app/code/Magento/CatalogInventory/Model/AddStockStatusToCollection.php @@ -7,6 +7,8 @@ namespace Magento\CatalogInventory\Model; use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Framework\Search\EngineResolverInterface; +use Magento\Search\Model\EngineResolver; /** * Catalog inventory module plugin @@ -17,18 +19,27 @@ class AddStockStatusToCollection * @var \Magento\CatalogInventory\Helper\Stock */ protected $stockHelper; - + + /** + * @var EngineResolverInterface + */ + private $engineResolver; + /** - * @param \Magento\CatalogInventory\Model\Configuration $configuration * @param \Magento\CatalogInventory\Helper\Stock $stockHelper + * @param EngineResolverInterface $engineResolver */ public function __construct( - \Magento\CatalogInventory\Helper\Stock $stockHelper + \Magento\CatalogInventory\Helper\Stock $stockHelper, + EngineResolverInterface $engineResolver ) { $this->stockHelper = $stockHelper; + $this->engineResolver = $engineResolver; } /** + * Add stock filter to collection. + * * @param Collection $productCollection * @param bool $printQuery * @param bool $logQuery @@ -36,7 +47,9 @@ public function __construct( */ public function beforeLoad(Collection $productCollection, $printQuery = false, $logQuery = false) { - $this->stockHelper->addIsInStockFilterToCollection($productCollection); + if ($this->engineResolver->getCurrentSearchEngine() === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE) { + $this->stockHelper->addIsInStockFilterToCollection($productCollection); + } return [$printQuery, $logQuery]; } } diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php index bc10d38173b4d..43a5aabee9779 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php @@ -6,12 +6,19 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogInventory\Model\Indexer\Stock\Action; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\CatalogInventory\Model\Indexer\Stock\BatchSizeManagement; +use Magento\CatalogInventory\Model\ResourceModel\Indexer\Stock\DefaultStock; use Magento\Framework\App\ResourceConnection; use Magento\CatalogInventory\Model\ResourceModel\Indexer\StockFactory; use Magento\Catalog\Model\Product\Type as ProductType; +use Magento\Framework\DB\Query\BatchIteratorInterface; +use Magento\Framework\DB\Query\Generator as QueryGenerator; use Magento\Framework\Indexer\CacheContext; use Magento\Framework\Event\ManagerInterface as EventManager; use Magento\Framework\EntityManager\MetadataPool; @@ -25,7 +32,6 @@ /** * Class Full reindex action * - * @package Magento\CatalogInventory\Model\Indexer\Stock\Action * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Full extends AbstractAction @@ -60,6 +66,11 @@ class Full extends AbstractAction */ private $activeTableSwitcher; + /** + * @var QueryGenerator|null + */ + private $batchQueryGenerator; + /** * @param ResourceConnection $resource * @param StockFactory $indexerFactory @@ -71,7 +82,7 @@ class Full extends AbstractAction * @param BatchProviderInterface|null $batchProvider * @param array $batchRowsCount * @param ActiveTableSwitcher|null $activeTableSwitcher - * + * @param QueryGenerator|null $batchQueryGenerator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -84,7 +95,8 @@ public function __construct( BatchSizeManagementInterface $batchSizeManagement = null, BatchProviderInterface $batchProvider = null, array $batchRowsCount = [], - ActiveTableSwitcher $activeTableSwitcher = null + ActiveTableSwitcher $activeTableSwitcher = null, + QueryGenerator $batchQueryGenerator = null ) { parent::__construct( $resource, @@ -97,11 +109,12 @@ public function __construct( $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class); $this->batchProvider = $batchProvider ?: ObjectManager::getInstance()->get(BatchProviderInterface::class); $this->batchSizeManagement = $batchSizeManagement ?: ObjectManager::getInstance()->get( - \Magento\CatalogInventory\Model\Indexer\Stock\BatchSizeManagement::class + BatchSizeManagement::class ); $this->batchRowsCount = $batchRowsCount; $this->activeTableSwitcher = $activeTableSwitcher ?: ObjectManager::getInstance() ->get(ActiveTableSwitcher::class); + $this->batchQueryGenerator = $batchQueryGenerator ?: ObjectManager::getInstance()->get(QueryGenerator::class); } /** @@ -109,22 +122,20 @@ public function __construct( * * @param null|array $ids * @throws LocalizedException - * * @return void - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function execute($ids = null) + public function execute($ids = null): void { try { $this->useIdxTable(false); $this->cleanIndexersTables($this->_getTypeIndexers()); - $entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); + $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); $columns = array_keys($this->_getConnection()->describeTable($this->_getIdxTable())); - /** @var \Magento\CatalogInventory\Model\ResourceModel\Indexer\Stock\DefaultStock $indexer */ + /** @var DefaultStock $indexer */ foreach ($this->_getTypeIndexers() as $indexer) { $indexer->setActionType(self::ACTION_TYPE); $connection = $indexer->getConnection(); @@ -135,22 +146,21 @@ public function execute($ids = null) : $this->batchRowsCount['default']; $this->batchSizeManagement->ensureBatchSize($connection, $batchRowCount); - $batches = $this->batchProvider->getBatches( - $connection, - $entityMetadata->getEntityTable(), + + $select = $connection->select(); + $select->distinct(true); + $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + + $batchQueries = $this->batchQueryGenerator->generate( $entityMetadata->getIdentifierField(), - $batchRowCount + $select, + $batchRowCount, + BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR ); - foreach ($batches as $batch) { + foreach ($batchQueries as $query) { $this->clearTemporaryIndexTable(); - // Get entity ids from batch - $select = $connection->select(); - $select->distinct(true); - $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); - $select->where('type_id = ?', $indexer->getTypeId()); - - $entityIds = $this->batchProvider->getBatchIds($connection, $select, $batch); + $entityIds = $connection->fetchCol($query); if (!empty($entityIds)) { $indexer->reindexEntity($entityIds); $select = $connection->select()->from($this->_getIdxTable(), $columns); @@ -167,12 +177,13 @@ public function execute($ids = null) /** * Delete all records from index table + * * Used to clean table before re-indexation * * @param array $indexers * @return void */ - private function cleanIndexersTables(array $indexers) + private function cleanIndexersTables(array $indexers): void { $tables = array_map( function (StockInterface $indexer) { diff --git a/app/code/Magento/CatalogInventory/Model/Plugin/Layer.php b/app/code/Magento/CatalogInventory/Model/Plugin/Layer.php index b8e8e47bb1fb0..168e947b8fa57 100644 --- a/app/code/Magento/CatalogInventory/Model/Plugin/Layer.php +++ b/app/code/Magento/CatalogInventory/Model/Plugin/Layer.php @@ -3,8 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogInventory\Model\Plugin; +use Magento\Framework\Search\EngineResolverInterface; +use Magento\Search\Model\EngineResolver; + +/** + * Catalog inventory plugin for layer. + */ class Layer { /** @@ -21,16 +28,24 @@ class Layer */ protected $scopeConfig; + /** + * @var EngineResolverInterface + */ + private $engineResolver; + /** * @param \Magento\CatalogInventory\Helper\Stock $stockHelper * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param EngineResolverInterface $engineResolver */ public function __construct( \Magento\CatalogInventory\Helper\Stock $stockHelper, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + EngineResolverInterface $engineResolver ) { $this->stockHelper = $stockHelper; $this->scopeConfig = $scopeConfig; + $this->engineResolver = $engineResolver; } /** @@ -46,12 +61,22 @@ public function beforePrepareProductCollection( \Magento\Catalog\Model\Layer $subject, \Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection $collection ) { - if ($this->_isEnabledShowOutOfStock()) { + if (!$this->isCurrentEngineMysql() || $this->_isEnabledShowOutOfStock()) { return; } $this->stockHelper->addIsInStockFilterToCollection($collection); } + /** + * Check if current engine is MYSQL. + * + * @return bool + */ + private function isCurrentEngineMysql() + { + return $this->engineResolver->getCurrentSearchEngine() === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE; + } + /** * Get config value for 'display out of stock' option * diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/AddStockStatusToCollectionTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/AddStockStatusToCollectionTest.php index af35666ced3e5..906df54732775 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/AddStockStatusToCollectionTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/AddStockStatusToCollectionTest.php @@ -6,6 +6,7 @@ namespace Magento\CatalogInventory\Test\Unit\Model; use Magento\CatalogInventory\Model\AddStockStatusToCollection; +use Magento\Framework\Search\EngineResolverInterface; class AddStockStatusToCollectionTest extends \PHPUnit\Framework\TestCase { @@ -19,13 +20,24 @@ class AddStockStatusToCollectionTest extends \PHPUnit\Framework\TestCase */ protected $stockHelper; + /** + * @var EngineResolverInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $engineResolver; + protected function setUp() { $this->stockHelper = $this->createMock(\Magento\CatalogInventory\Helper\Stock::class); + $this->engineResolver = $this->getMockBuilder(EngineResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getCurrentSearchEngine']) + ->getMockForAbstractClass(); + $this->plugin = (new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this))->getObject( \Magento\CatalogInventory\Model\AddStockStatusToCollection::class, [ 'stockHelper' => $this->stockHelper, + 'engineResolver' => $this->engineResolver ] ); } @@ -36,6 +48,10 @@ public function testAddStockStatusToCollection() ->disableOriginalConstructor() ->getMock(); + $this->engineResolver->expects($this->any()) + ->method('getCurrentSearchEngine') + ->willReturn('mysql'); + $this->stockHelper->expects($this->once()) ->method('addIsInStockFilterToCollection') ->with($productCollection) diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/LayerTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/LayerTest.php index 287459bd8cbc8..b64563a35176d 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/LayerTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/LayerTest.php @@ -5,6 +5,8 @@ */ namespace Magento\CatalogInventory\Test\Unit\Model\Plugin; +use Magento\Framework\Search\EngineResolverInterface; + class LayerTest extends \PHPUnit\Framework\TestCase { /** @@ -22,14 +24,24 @@ class LayerTest extends \PHPUnit\Framework\TestCase */ protected $_stockHelperMock; + /** + * @var EngineResolverInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $engineResolver; + protected function setUp() { $this->_scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); $this->_stockHelperMock = $this->createMock(\Magento\CatalogInventory\Helper\Stock::class); + $this->engineResolver = $this->getMockBuilder(EngineResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getCurrentSearchEngine']) + ->getMockForAbstractClass(); $this->_model = new \Magento\CatalogInventory\Model\Plugin\Layer( $this->_stockHelperMock, - $this->_scopeConfigMock + $this->_scopeConfigMock, + $this->engineResolver ); } @@ -38,6 +50,10 @@ protected function setUp() */ public function testAddStockStatusDisabledShow() { + $this->engineResolver->expects($this->any()) + ->method('getCurrentSearchEngine') + ->willReturn('mysql'); + $this->_scopeConfigMock->expects( $this->once() )->method( @@ -60,6 +76,10 @@ public function testAddStockStatusDisabledShow() */ public function testAddStockStatusEnabledShow() { + $this->engineResolver->expects($this->any()) + ->method('getCurrentSearchEngine') + ->willReturn('mysql'); + $this->_scopeConfigMock->expects( $this->once() )->method( diff --git a/app/code/Magento/CatalogInventory/composer.json b/app/code/Magento/CatalogInventory/composer.json index 007d744b2296f..eb6239ea87ef0 100644 --- a/app/code/Magento/CatalogInventory/composer.json +++ b/app/code/Magento/CatalogInventory/composer.json @@ -8,6 +8,7 @@ "php": "~7.1.3||~7.2.0", "magento/framework": "*", "magento/module-catalog": "*", + "magento/module-search": "*", "magento/module-config": "*", "magento/module-customer": "*", "magento/module-eav": "*", diff --git a/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php b/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php index 184fd9cfd5b37..384132415a10e 100644 --- a/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php +++ b/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php @@ -60,15 +60,9 @@ public function execute() { try { $this->_catalogSearchAdvanced->addFilters($this->getRequest()->getQueryValue()); - $size = $this->_catalogSearchAdvanced->getProductCollection()->getSize(); - - $handles = null; - if ($size == 0) { - $this->_view->getPage()->initLayout(); - $handles = $this->_view->getLayout()->getUpdate()->getHandles(); - $handles[] = static::DEFAULT_NO_RESULT_HANDLE; - } - + $this->_view->getPage()->initLayout(); + $handles = $this->_view->getLayout()->getUpdate()->getHandles(); + $handles[] = static::DEFAULT_NO_RESULT_HANDLE; $this->_view->loadLayout($handles); $this->_view->renderLayout(); } catch (\Magento\Framework\Exception\LocalizedException $e) { diff --git a/app/code/Magento/CatalogSearch/Model/Advanced.php b/app/code/Magento/CatalogSearch/Model/Advanced.php index af0e9ff5528cf..5b96a8c21cbea 100644 --- a/app/code/Magento/CatalogSearch/Model/Advanced.php +++ b/app/code/Magento/CatalogSearch/Model/Advanced.php @@ -6,6 +6,8 @@ namespace Magento\CatalogSearch\Model; use Magento\Catalog\Model\Config; +use Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyProvider; +use Magento\CatalogSearch\Model\Search\ItemCollectionProviderInterface; use Magento\Catalog\Model\Product\Visibility; use Magento\Catalog\Model\ProductFactory; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; @@ -19,6 +21,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Registry; use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\App\ObjectManager; /** * Catalog advanced search model @@ -64,6 +67,7 @@ class Advanced extends \Magento\Framework\Model\AbstractModel /** * Initialize dependencies * + * @deprecated * @var Config */ protected $_catalogConfig; @@ -106,10 +110,22 @@ class Advanced extends \Magento\Framework\Model\AbstractModel /** * Advanced Collection Factory * + * @deprecated + * @see $collectionProvider * @var ProductCollectionFactory */ protected $productCollectionFactory; + /** + * @var ItemCollectionProviderInterface + */ + private $collectionProvider; + + /** + * @var ProductCollectionPrepareStrategyProvider|null + */ + private $productCollectionPrepareStrategyProvider; + /** * Construct * @@ -124,7 +140,8 @@ class Advanced extends \Magento\Framework\Model\AbstractModel * @param ProductCollectionFactory $productCollectionFactory * @param AdvancedFactory $advancedFactory * @param array $data - * + * @param ItemCollectionProviderInterface|null $collectionProvider + * @param ProductCollectionPrepareStrategyProvider|null $productCollectionPrepareStrategyProvider * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -138,7 +155,9 @@ public function __construct( StoreManagerInterface $storeManager, ProductCollectionFactory $productCollectionFactory, AdvancedFactory $advancedFactory, - array $data = [] + array $data = [], + ItemCollectionProviderInterface $collectionProvider = null, + ProductCollectionPrepareStrategyProvider $productCollectionPrepareStrategyProvider = null ) { $this->_attributeCollectionFactory = $attributeCollectionFactory; $this->_catalogProductVisibility = $catalogProductVisibility; @@ -147,11 +166,14 @@ public function __construct( $this->_productFactory = $productFactory; $this->_storeManager = $storeManager; $this->productCollectionFactory = $productCollectionFactory; + $this->collectionProvider = $collectionProvider; + $this->productCollectionPrepareStrategyProvider = $productCollectionPrepareStrategyProvider + ?: ObjectManager::getInstance()->get(ProductCollectionPrepareStrategyProvider::class); parent::__construct( $context, $registry, $advancedFactory->create(), - $this->productCollectionFactory->create(), + $this->resolveProductCollection(), $data ); } @@ -266,7 +288,7 @@ public function getAttributes() public function getProductCollection() { if ($this->_productCollection === null) { - $collection = $this->productCollectionFactory->create(); + $collection = $this->resolveProductCollection(); $this->prepareProductCollection($collection); if (!$collection) { return $collection; @@ -277,6 +299,18 @@ public function getProductCollection() return $this->_productCollection; } + /** + * Resolve product collection. + * + * @return \Magento\Catalog\Model\ResourceModel\Product\Collection|\Magento\Framework\Data\Collection + */ + private function resolveProductCollection() + { + return (null === $this->collectionProvider) + ? $this->productCollectionFactory->create() + : $this->collectionProvider->getCollection(); + } + /** * Prepare product collection * @@ -285,13 +319,7 @@ public function getProductCollection() */ public function prepareProductCollection($collection) { - $collection - ->addAttributeToSelect($this->_catalogConfig->getProductAttributes()) - ->setStore($this->_storeManager->getStore()) - ->addMinimalPrice() - ->addTaxPercents() - ->addStoreFilter() - ->setVisibility($this->_catalogProductVisibility->getVisibleInSearchIds()); + $this->productCollectionPrepareStrategyProvider->getStrategy()->prepare($collection); return $this; } diff --git a/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategy.php b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategy.php new file mode 100644 index 0000000000000..e62de9c689fc2 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategy.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogSearch\Model\Advanced; + +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\Config; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Strategy interface for preparing product collection. + */ +class ProductCollectionPrepareStrategy implements ProductCollectionPrepareStrategyInterface +{ + /** + * @var Config + */ + private $catalogConfig; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var Visibility + */ + private $catalogProductVisibility; + + /** + * @param Config $catalogConfig + * @param StoreManagerInterface $storeManager + * @param Visibility $catalogProductVisibility + */ + public function __construct( + Config $catalogConfig, + StoreManagerInterface $storeManager, + Visibility $catalogProductVisibility + ) { + $this->catalogConfig = $catalogConfig; + $this->storeManager = $storeManager; + $this->catalogProductVisibility = $catalogProductVisibility; + } + + /** + * @inheritdoc + */ + public function prepare(Collection $collection) + { + $collection + ->addAttributeToSelect($this->catalogConfig->getProductAttributes()) + ->setStore($this->storeManager->getStore()) + ->addMinimalPrice() + ->addTaxPercents() + ->addStoreFilter() + ->setVisibility($this->catalogProductVisibility->getVisibleInSearchIds()); + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyInterface.php b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyInterface.php new file mode 100644 index 0000000000000..23719a6713a32 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogSearch\Model\Advanced; + +use Magento\Catalog\Model\ResourceModel\Product\Collection; + +/** + * Strategy interface for preparing product collection. + */ +interface ProductCollectionPrepareStrategyInterface +{ + /** + * Prepare product collection. + * + * @param Collection $collection + * @return void + */ + public function prepare(Collection $collection); +} diff --git a/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyProvider.php b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyProvider.php new file mode 100644 index 0000000000000..6e963ea1aa8ac --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyProvider.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogSearch\Model\Advanced; + +use Magento\Framework\Search\EngineResolverInterface; + +/** + * Strategy provider for preparing product collection. + */ +class ProductCollectionPrepareStrategyProvider +{ + /** + * @var EngineResolverInterface + */ + private $engineResolver; + + /** + * @var array + */ + private $strategies; + + /** + * @param EngineResolverInterface $engineResolver + * @param array $strategies + */ + public function __construct( + EngineResolverInterface $engineResolver, + array $strategies + ) { + $this->engineResolver = $engineResolver; + $this->strategies = $strategies; + } + + /** + * Get strategy provider for product collection prepare process. + * + * @return ProductCollectionPrepareStrategyInterface + */ + public function getStrategy(): ProductCollectionPrepareStrategyInterface + { + if (!isset($this->strategies[$this->engineResolver->getCurrentSearchEngine()])) { + throw new \DomainException('Undefined strategy ' . $this->engineResolver->getCurrentSearchEngine()); + } + return $this->strategies[$this->engineResolver->getCurrentSearchEngine()]; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php index 2d175f684b0f7..e3f61af771f8c 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php @@ -9,17 +9,27 @@ use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; +use Magento\Framework\Search\EngineResolverInterface; +use Magento\Search\Model\EngineResolver; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\DB\Select; use Magento\Framework\Api\Search\SearchCriteriaBuilder; use Magento\Framework\Api\Search\SearchResultFactory; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Indexer\DimensionFactory; use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; -use Magento\Framework\Search\Adapter\Mysql\TemporaryStorage; use Magento\Framework\Search\Request\EmptyRequestDataException; use Magento\Framework\Search\Request\NonExistingRequestNameException; +use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Api\Search\SearchResultInterface; /** * Advanced search collection @@ -65,6 +75,41 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection */ private $filterBuilder; + /** + * @var \Magento\Framework\Api\Search\SearchResultInterface + */ + private $searchResult; + + /** + * @var string + */ + private $searchRequestName; + + /** + * @var SearchCriteriaResolverFactory + */ + private $searchCriteriaResolverFactory; + + /** + * @var SearchResultApplierFactory + */ + private $searchResultApplierFactory; + + /** + * @var TotalRecordsResolverFactory + */ + private $totalRecordsResolverFactory; + + /** + * @var EngineResolverInterface + */ + private $engineResolver; + + /** + * @var array + */ + private $searchOrders; + /** * Collection constructor * @@ -93,11 +138,16 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection * @param SearchResultFactory|null $searchResultFactory * @param ProductLimitationFactory|null $productLimitationFactory - * @param MetadataPool|null $metadataPool * + * @param MetadataPool|null $metadataPool * @param TableMaintainer|null $tableMaintainer * @param PriceTableResolver|null $priceTableResolver * @param DimensionFactory|null $dimensionFactory * @param ResourceModelPoolInterface|null $resourceModelPool + * @param string $searchRequestName + * @param SearchCriteriaResolverFactory|null $searchCriteriaResolverFactory + * @param SearchResultApplierFactory|null $searchResultApplierFactory + * @param TotalRecordsResolverFactory|null $totalRecordsResolverFactory + * @param EngineResolverInterface|null $engineResolver * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -130,15 +180,29 @@ public function __construct( TableMaintainer $tableMaintainer = null, PriceTableResolver $priceTableResolver = null, DimensionFactory $dimensionFactory = null, - ResourceModelPoolInterface $resourceModelPool = null + ResourceModelPoolInterface $resourceModelPool = null, + $searchRequestName = 'advanced_search_container', + SearchCriteriaResolverFactory $searchCriteriaResolverFactory = null, + SearchResultApplierFactory $searchResultApplierFactory = null, + TotalRecordsResolverFactory $totalRecordsResolverFactory = null, + EngineResolverInterface $engineResolver = null ) { $this->requestBuilder = $requestBuilder; $this->searchEngine = $searchEngine; $this->temporaryStorageFactory = $temporaryStorageFactory; + $this->searchRequestName = $searchRequestName; if ($searchResultFactory === null) { $this->searchResultFactory = \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Api\Search\SearchResultFactory::class); } + $this->searchCriteriaResolverFactory = $searchCriteriaResolverFactory ?: ObjectManager::getInstance() + ->get(SearchCriteriaResolverFactory::class); + $this->searchResultApplierFactory = $searchResultApplierFactory ?: ObjectManager::getInstance() + ->get(SearchResultApplierFactory::class); + $this->totalRecordsResolverFactory = $totalRecordsResolverFactory ?: ObjectManager::getInstance() + ->get(TotalRecordsResolverFactory::class); + $this->engineResolver = $engineResolver ?: ObjectManager::getInstance() + ->get(EngineResolverInterface::class); parent::__construct( $entityFactory, $logger, @@ -184,11 +248,89 @@ public function addFieldsToFilter($fields) return $this; } + /** + * @inheritdoc + */ + public function setOrder($attribute, $dir = Select::SQL_DESC) + { + $this->setSearchOrder($attribute, $dir); + if ($this->isCurrentEngineMysql()) { + parent::setOrder($attribute, $dir); + } + + return $this; + } + + /** + * @inheritdoc + */ + public function addCategoryFilter(\Magento\Catalog\Model\Category $category) + { + /** + * This changes need in backward compatible reasons for support dynamic improved algorithm + * for price aggregation process. + */ + if ($this->isCurrentEngineMysql()) { + parent::addCategoryFilter($category); + } else { + $this->addFieldToFilter('category_ids', $category->getId()); + $this->_productLimitationPrice(); + } + + return $this; + } + + /** + * @inheritdoc + */ + public function setVisibility($visibility) + { + /** + * This changes need in backward compatible reasons for support dynamic improved algorithm + * for price aggregation process. + */ + if ($this->isCurrentEngineMysql()) { + parent::setVisibility($visibility); + } else { + $this->addFieldToFilter('visibility', $visibility); + } + + return $this; + } + + /** + * Set sort order for search query. + * + * @param string $field + * @param string $direction + * @return void + */ + private function setSearchOrder($field, $direction) + { + $field = (string)$this->_getMappedField($field); + $direction = strtoupper($direction) == self::SORT_ORDER_ASC ? self::SORT_ORDER_ASC : self::SORT_ORDER_DESC; + + $this->searchOrders[$field] = $direction; + } + + /** + * Check if current engine is MYSQL. + * + * @return bool + */ + private function isCurrentEngineMysql() + { + return $this->engineResolver->getCurrentSearchEngine() === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE; + } + /** * @inheritdoc */ protected function _renderFiltersBefore() { + if ($this->isLoaded()) { + return; + } if ($this->filters) { foreach ($this->filters as $attributes) { foreach ($attributes as $attributeCode => $attributeValue) { @@ -196,33 +338,70 @@ protected function _renderFiltersBefore() $this->addAttributeToSearch($attributeCode, $attributeValue); } } - $searchCriteria = $this->getSearchCriteriaBuilder()->create(); - $searchCriteria->setRequestName('advanced_search_container'); + $searchCriteria = $this->getSearchCriteriaResolver()->resolve(); try { - $searchResult = $this->getSearch()->search($searchCriteria); + $this->searchResult = $this->getSearch()->search($searchCriteria); + $this->_totalRecords = $this->getTotalRecordsResolver($this->searchResult)->resolve(); } catch (EmptyRequestDataException $e) { /** @var \Magento\Framework\Api\Search\SearchResultInterface $searchResult */ - $searchResult = $this->searchResultFactory->create()->setItems([]); + $this->searchResult = $this->searchResultFactory->create()->setItems([]); } catch (NonExistingRequestNameException $e) { $this->_logger->error($e->getMessage()); throw new LocalizedException( __('An error occurred. For details, see the error log.') ); } - $temporaryStorage = $this->temporaryStorageFactory->create(); - $table = $temporaryStorage->storeApiDocuments($searchResult->getItems()); - - $this->getSelect()->joinInner( - [ - 'search_result' => $table->getName(), - ], - 'e.entity_id = search_result.' . TemporaryStorage::FIELD_ENTITY_ID, - [] - ); + $this->getSearchResultApplier($this->searchResult)->apply(); } parent::_renderFiltersBefore(); } + /** + * Get total records resolver. + * + * @param SearchResultInterface $searchResult + * @return TotalRecordsResolverInterface + */ + private function getTotalRecordsResolver(SearchResultInterface $searchResult): TotalRecordsResolverInterface + { + return $this->totalRecordsResolverFactory->create([ + 'searchResult' => $searchResult, + ]); + } + + /** + * Get search criteria resolver. + * + * @return SearchCriteriaResolverInterface + */ + private function getSearchCriteriaResolver(): SearchCriteriaResolverInterface + { + return $this->searchCriteriaResolverFactory->create([ + 'builder' => $this->getSearchCriteriaBuilder(), + 'collection' => $this, + 'searchRequestName' => $this->searchRequestName, + 'currentPage' => $this->_curPage, + 'size' => $this->getPageSize(), + 'orders' => $this->searchOrders, + ]); + } + + /** + * Get search result applier. + * + * @param SearchResultInterface $searchResult + * @return SearchResultApplierInterface + */ + private function getSearchResultApplier(SearchResultInterface $searchResult): SearchResultApplierInterface + { + return $this->searchResultApplierFactory->create([ + 'collection' => $this, + 'searchResult' => $searchResult, + /** This variable sets by serOrder method, but doesn't have a getter method. */ + 'orders' => $this->_orders + ]); + } + /** * Get attribute code. * diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php index 3ba77e77105ae..2c36b150fed07 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php @@ -8,20 +8,29 @@ use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; -use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; -use Magento\CatalogSearch\Model\Search\RequestGenerator; -use Magento\Framework\Api\Search\SearchResultFactory; -use Magento\Framework\App\ObjectManager; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; +use Magento\Framework\Search\EngineResolverInterface; +use Magento\Framework\Data\Collection\Db\SizeResolverInterfaceFactory; use Magento\Framework\DB\Select; +use Magento\Framework\Api\Search\SearchResultInterface; +use Magento\CatalogSearch\Model\Search\RequestGenerator; use Magento\Framework\EntityManager\MetadataPool; -use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\StateException; use Magento\Framework\Indexer\DimensionFactory; use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; -use Magento\Framework\Search\Adapter\Mysql\TemporaryStorage; +use Magento\Framework\Search\Response\QueryResponse; use Magento\Framework\Search\Request\EmptyRequestDataException; use Magento\Framework\Search\Request\NonExistingRequestNameException; -use Magento\Framework\Search\Response\QueryResponse; +use Magento\Framework\Api\Search\SearchResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\App\ObjectManager; +use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\Search\Model\EngineResolver; /** * Fulltext Collection @@ -31,6 +40,7 @@ * @api * @since 100.0.2 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) */ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection { @@ -65,11 +75,6 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection */ private $queryText; - /** - * @var string|null - */ - private $relevanceOrderDirection = null; - /** * @var string */ @@ -106,6 +111,31 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection */ private $filterBuilder; + /** + * @var SearchCriteriaResolverFactory + */ + private $searchCriteriaResolverFactory; + + /** + * @var SearchResultApplierFactory + */ + private $searchResultApplierFactory; + + /** + * @var TotalRecordsResolverFactory + */ + private $totalRecordsResolverFactory; + + /** + * @var EngineResolverInterface + */ + private $engineResolver; + + /** + * @var array + */ + private $searchOrders; + /** * Collection constructor * @@ -141,7 +171,15 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param PriceTableResolver|null $priceTableResolver * @param DimensionFactory|null $dimensionFactory * @param ResourceModelPoolInterface|null $resourceModelPool + * @param \Magento\Search\Api\SearchInterface|null $search + * @param \Magento\Framework\Api\Search\SearchCriteriaBuilder|null $searchCriteriaBuilder + * @param \Magento\Framework\Api\FilterBuilder|null $filterBuilder + * @param SearchCriteriaResolverFactory|null $searchCriteriaResolverFactory + * @param SearchResultApplierFactory|null $searchResultApplierFactory + * @param TotalRecordsResolverFactory|null $totalRecordsResolverFactory + * @param EngineResolverInterface|null $engineResolver * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function __construct( \Magento\Framework\Data\Collection\EntityFactory $entityFactory, @@ -175,7 +213,14 @@ public function __construct( TableMaintainer $tableMaintainer = null, PriceTableResolver $priceTableResolver = null, DimensionFactory $dimensionFactory = null, - ResourceModelPoolInterface $resourceModelPool = null + ResourceModelPoolInterface $resourceModelPool = null, + \Magento\Search\Api\SearchInterface $search = null, + \Magento\Framework\Api\Search\SearchCriteriaBuilder $searchCriteriaBuilder = null, + \Magento\Framework\Api\FilterBuilder $filterBuilder = null, + SearchCriteriaResolverFactory $searchCriteriaResolverFactory = null, + SearchResultApplierFactory $searchResultApplierFactory = null, + TotalRecordsResolverFactory $totalRecordsResolverFactory = null, + EngineResolverInterface $engineResolver = null ) { $this->queryFactory = $catalogSearchData; if ($searchResultFactory === null) { @@ -214,6 +259,19 @@ public function __construct( $this->searchEngine = $searchEngine; $this->temporaryStorageFactory = $temporaryStorageFactory; $this->searchRequestName = $searchRequestName; + $this->search = $search ?: ObjectManager::getInstance()->get(\Magento\Search\Api\SearchInterface::class); + $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: ObjectManager::getInstance() + ->get(\Magento\Framework\Api\Search\SearchCriteriaBuilder::class); + $this->filterBuilder = $filterBuilder ?: ObjectManager::getInstance() + ->get(\Magento\Framework\Api\FilterBuilder::class); + $this->searchCriteriaResolverFactory = $searchCriteriaResolverFactory ?: ObjectManager::getInstance() + ->get(SearchCriteriaResolverFactory::class); + $this->searchResultApplierFactory = $searchResultApplierFactory ?: ObjectManager::getInstance() + ->get(SearchResultApplierFactory::class); + $this->totalRecordsResolverFactory = $totalRecordsResolverFactory ?: ObjectManager::getInstance() + ->get(TotalRecordsResolverFactory::class); + $this->engineResolver = $engineResolver ?: ObjectManager::getInstance() + ->get(EngineResolverInterface::class); } /** @@ -347,32 +405,32 @@ public function addSearchFilter($query) /** * @inheritdoc */ - protected function _renderFiltersBefore() + public function setOrder($attribute, $dir = Select::SQL_DESC) { - $this->getSearchCriteriaBuilder(); - $this->getFilterBuilder(); - $this->getSearch(); - - if ($this->queryText) { - $this->filterBuilder->setField('search_term'); - $this->filterBuilder->setValue($this->queryText); - $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); + $this->setSearchOrder($attribute, $dir); + if ($this->isCurrentEngineMysql()) { + parent::setOrder($attribute, $dir); } - $priceRangeCalculation = $this->_scopeConfig->getValue( - \Magento\Catalog\Model\Layer\Filter\Dynamic\AlgorithmFactory::XML_PATH_RANGE_CALCULATION, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - if ($priceRangeCalculation) { - $this->filterBuilder->setField('price_dynamic_algorithm'); - $this->filterBuilder->setValue($priceRangeCalculation); - $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); + return $this; + } + + /** + * @inheritdoc + */ + protected function _renderFiltersBefore() + { + if ($this->isLoaded()) { + return; } - $searchCriteria = $this->searchCriteriaBuilder->create(); - $searchCriteria->setRequestName($this->searchRequestName); + $this->prepareSearchTermFilter(); + $this->preparePriceAggregation(); + + $searchCriteria = $this->getSearchCriteriaResolver()->resolve(); try { $this->searchResult = $this->getSearch()->search($searchCriteria); + $this->_totalRecords = $this->getTotalRecordsResolver($this->searchResult)->resolve(); } catch (EmptyRequestDataException $e) { /** @var \Magento\Framework\Api\Search\SearchResultInterface $searchResult */ $this->searchResult = $this->searchResultFactory->create()->setItems([]); @@ -381,23 +439,79 @@ protected function _renderFiltersBefore() throw new LocalizedException(__('An error occurred. For details, see the error log.')); } - $temporaryStorage = $this->temporaryStorageFactory->create(); - $table = $temporaryStorage->storeApiDocuments($this->searchResult->getItems()); + $this->getSearchResultApplier($this->searchResult)->apply(); + parent::_renderFiltersBefore(); + } - $this->getSelect()->joinInner( - [ - 'search_result' => $table->getName(), - ], - 'e.entity_id = search_result.' . TemporaryStorage::FIELD_ENTITY_ID, - [] - ); + /** + * Set sort order for search query. + * + * @param string $field + * @param string $direction + * @return void + */ + private function setSearchOrder($field, $direction) + { + $field = (string)$this->_getMappedField($field); + $direction = strtoupper($direction) == self::SORT_ORDER_ASC ? self::SORT_ORDER_ASC : self::SORT_ORDER_DESC; - if ($this->relevanceOrderDirection) { - $this->getSelect()->order( - 'search_result.' . TemporaryStorage::FIELD_SCORE . ' ' . $this->relevanceOrderDirection - ); - } - return parent::_renderFiltersBefore(); + $this->searchOrders[$field] = $direction; + } + + /** + * Check if current engine is MYSQL. + * + * @return bool + */ + private function isCurrentEngineMysql() + { + return $this->engineResolver->getCurrentSearchEngine() === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE; + } + + /** + * Get total records resolver. + * + * @param SearchResultInterface $searchResult + * @return TotalRecordsResolverInterface + */ + private function getTotalRecordsResolver(SearchResultInterface $searchResult): TotalRecordsResolverInterface + { + return $this->totalRecordsResolverFactory->create([ + 'searchResult' => $searchResult, + ]); + } + + /** + * Get search criteria resolver. + * + * @return SearchCriteriaResolverInterface + */ + private function getSearchCriteriaResolver(): SearchCriteriaResolverInterface + { + return $this->searchCriteriaResolverFactory->create([ + 'builder' => $this->getSearchCriteriaBuilder(), + 'collection' => $this, + 'searchRequestName' => $this->searchRequestName, + 'currentPage' => $this->_curPage, + 'size' => $this->getPageSize(), + 'orders' => $this->searchOrders, + ]); + } + + /** + * Get search result applier. + * + * @param SearchResultInterface $searchResult + * @return SearchResultApplierInterface + */ + private function getSearchResultApplier(SearchResultInterface $searchResult): SearchResultApplierInterface + { + return $this->searchResultApplierFactory->create([ + 'collection' => $this, + 'searchResult' => $searchResult, + /** This variable sets by serOrder method, but doesn't have a getter method. */ + 'orders' => $this->_orders, + ]); } /** @@ -425,24 +539,6 @@ protected function _renderFilters() return parent::_renderFilters(); } - /** - * Set Order field - * - * @param string $attribute - * @param string $dir - * @return $this - */ - public function setOrder($attribute, $dir = Select::SQL_DESC) - { - if ($attribute === 'relevance') { - $this->relevanceOrderDirection = $dir; - } else { - parent::setOrder($attribute, $dir); - } - - return $this; - } - /** * Stub method for compatibility with other search engines * @@ -489,7 +585,17 @@ public function getFacetedData($field) public function addCategoryFilter(\Magento\Catalog\Model\Category $category) { $this->addFieldToFilter('category_ids', $category->getId()); - return parent::addCategoryFilter($category); + /** + * This changes need in backward compatible reasons for support dynamic improved algorithm + * for price aggregation process. + */ + if ($this->isCurrentEngineMysql()) { + parent::addCategoryFilter($category); + } else { + $this->_productLimitationPrice(); + } + + return $this; } /** @@ -501,6 +607,46 @@ public function addCategoryFilter(\Magento\Catalog\Model\Category $category) public function setVisibility($visibility) { $this->addFieldToFilter('visibility', $visibility); - return parent::setVisibility($visibility); + /** + * This changes need in backward compatible reasons for support dynamic improved algorithm + * for price aggregation process. + */ + if ($this->isCurrentEngineMysql()) { + parent::setVisibility($visibility); + } + + return $this; + } + + /** + * Prepare search term filter for text query. + * + * @return void + */ + private function prepareSearchTermFilter(): void + { + if ($this->queryText) { + $this->filterBuilder->setField('search_term'); + $this->filterBuilder->setValue($this->queryText); + $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); + } + } + + /** + * Prepare price aggregation algorithm. + * + * @return void + */ + private function preparePriceAggregation(): void + { + $priceRangeCalculation = $this->_scopeConfig->getValue( + \Magento\Catalog\Model\Layer\Filter\Dynamic\AlgorithmFactory::XML_PATH_RANGE_CALCULATION, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + if ($priceRangeCalculation) { + $this->filterBuilder->setField('price_dynamic_algorithm'); + $this->filterBuilder->setValue($priceRangeCalculation); + $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); + } } } diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php new file mode 100644 index 0000000000000..632e1ab9270df --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; + +use Magento\Framework\Data\Collection; +use Magento\Framework\Api\Search\SearchCriteriaBuilder; +use Magento\Framework\Api\Search\SearchCriteria; + +/** + * Resolve specific attributes for search criteria. + */ +class SearchCriteriaResolver implements SearchCriteriaResolverInterface +{ + /** + * @var SearchCriteriaBuilder + */ + private $builder; + + /** + * @var string + */ + private $searchRequestName; + + /** + * SearchCriteriaResolver constructor. + * @param SearchCriteriaBuilder $builder + * @param string $searchRequestName + */ + public function __construct( + SearchCriteriaBuilder $builder, + string $searchRequestName + ) { + $this->builder = $builder; + $this->searchRequestName = $searchRequestName; + } + + /** + * @inheritdoc + */ + public function resolve() : SearchCriteria + { + $searchCriteria = $this->builder->create(); + $searchCriteria->setRequestName($this->searchRequestName); + + return $searchCriteria; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverInterface.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverInterface.php new file mode 100644 index 0000000000000..047fa7f71e400 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; + +use Magento\Framework\Api\Search\SearchCriteria; + +/** + * Resolve specific attributes for search criteria. + */ +interface SearchCriteriaResolverInterface +{ + /** + * Resolve specific attribute. + * + * @return SearchCriteria + */ + public function resolve(): SearchCriteria; +} diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php new file mode 100644 index 0000000000000..20c237646e524 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; + +use Magento\Framework\Data\Collection; +use Magento\Framework\Search\Adapter\Mysql\TemporaryStorage; +use Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory; +use Magento\Framework\Api\Search\SearchResultInterface; + +/** + * Resolve specific attributes for search criteria. + */ +class SearchResultApplier implements SearchResultApplierInterface +{ + /** + * @var Collection + */ + private $collection; + + /** + * @var SearchResultInterface + */ + private $searchResult; + + /** + * @var TemporaryStorageFactory + */ + private $temporaryStorageFactory; + + /** + * @var array + */ + private $orders; + + /** + * @param Collection $collection + * @param SearchResultInterface $searchResult + * @param TemporaryStorageFactory $temporaryStorageFactory + * @param array $orders + */ + public function __construct( + Collection $collection, + SearchResultInterface $searchResult, + TemporaryStorageFactory $temporaryStorageFactory, + array $orders + ) { + $this->collection = $collection; + $this->searchResult = $searchResult; + $this->temporaryStorageFactory = $temporaryStorageFactory; + $this->orders = $orders; + } + + /** + * @inheritdoc + */ + public function apply() + { + $temporaryStorage = $this->temporaryStorageFactory->create(); + $table = $temporaryStorage->storeApiDocuments($this->searchResult->getItems()); + + $this->collection->getSelect()->joinInner( + [ + 'search_result' => $table->getName(), + ], + 'e.entity_id = search_result.' . TemporaryStorage::FIELD_ENTITY_ID, + [] + ); + + if (isset($this->orders['relevance'])) { + $this->collection->getSelect()->order( + 'search_result.' . TemporaryStorage::FIELD_SCORE . ' ' . $this->orders['relevance'] + ); + } + } +} diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplierInterface.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplierInterface.php new file mode 100644 index 0000000000000..1b3e2a6bbac71 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplierInterface.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; + +/** + * Resolve specific attributes for search criteria. + */ +interface SearchResultApplierInterface +{ + /** + * Apply search results to collection. + * + * @return void + */ + public function apply(); +} diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolver.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolver.php new file mode 100644 index 0000000000000..12b6f81313913 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolver.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; + +/** + * Resolve total records count. + * + * For Mysql search engine we can't resolve total record count before full load of collection. + */ +class TotalRecordsResolver implements TotalRecordsResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve(): ?int + { + return null; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolverInterface.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolverInterface.php new file mode 100644 index 0000000000000..190450f9606bc --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolverInterface.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; + +/** + * Resolve total records count. + */ +interface TotalRecordsResolverInterface +{ + /** + * Resolve total records. + * + * @return int + */ + public function resolve(): ?int; +} diff --git a/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProvider.php b/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProvider.php new file mode 100644 index 0000000000000..f621bcbf91835 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProvider.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Model\Search; + +use Magento\Framework\Search\EngineResolverInterface; +use Magento\Framework\Data\Collection; + +/** + * Search collection provider. + */ +class ItemCollectionProvider implements ItemCollectionProviderInterface +{ + /** + * @var EngineResolverInterface + */ + private $engineResolver; + + /** + * @var array + */ + private $factories; + + /** + * ItemCollectionProvider constructor. + * @param EngineResolverInterface $engineResolver + * @param array $factories + */ + public function __construct( + EngineResolverInterface $engineResolver, + array $factories + ) { + $this->engineResolver = $engineResolver; + $this->factories = $factories; + } + + /** + * @inheritdoc + */ + public function getCollection(): Collection + { + if (!isset($this->factories[$this->engineResolver->getCurrentSearchEngine()])) { + throw new \DomainException('Undefined factory ' . $this->engineResolver->getCurrentSearchEngine()); + } + return $this->factories[$this->engineResolver->getCurrentSearchEngine()]->create(); + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProviderInterface.php b/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProviderInterface.php new file mode 100644 index 0000000000000..db02d5ac5f519 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProviderInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Model\Search; + +use Magento\Framework\Data\Collection; + +/** + * Search collection provider. + */ +interface ItemCollectionProviderInterface +{ + /** + * Get collection. + * + * @return Collection + */ + public function getCollection() : Collection; +} diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php index 2faacea24262c..8d1dbbd6dd6df 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php @@ -20,7 +20,17 @@ public function testResultActionFiltersSetBeforeLoadLayout() $filters = null; $expectedQuery = 'filtersData'; - $view = $this->createPartialMock(\Magento\Framework\App\View::class, ['loadLayout', 'renderLayout']); + $view = $this->createPartialMock( + \Magento\Framework\App\View::class, + ['loadLayout', 'renderLayout', 'getPage', 'getLayout'] + ); + $update = $this->createPartialMock(\Magento\Framework\View\Model\Layout\Merge::class, ['getHandles']); + $update->expects($this->once())->method('getHandles')->will($this->returnValue([])); + $layout = $this->createPartialMock(\Magento\Framework\View\Result\Layout::class, ['getUpdate']); + $layout->expects($this->once())->method('getUpdate')->will($this->returnValue($update)); + $view->expects($this->once())->method('getLayout')->will($this->returnValue($layout)); + $page = $this->createPartialMock(\Magento\Framework\View\Result\Page::class, ['initLayout']); + $view->expects($this->once())->method('getPage')->will($this->returnValue($page)); $view->expects($this->once())->method('loadLayout')->will( $this->returnCallback( function () use (&$filters, $expectedQuery) { @@ -32,15 +42,9 @@ function () use (&$filters, $expectedQuery) { $request = $this->createPartialMock(\Magento\Framework\App\Console\Request::class, ['getQueryValue']); $request->expects($this->once())->method('getQueryValue')->will($this->returnValue($expectedQuery)); - $collection = $this->createPartialMock( - \Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection::class, - ['getSize'] - ); - $collection->expects($this->once())->method('getSize')->will($this->returnValue(1)); - $catalogSearchAdvanced = $this->createPartialMock( \Magento\CatalogSearch\Model\Advanced::class, - ['addFilters', '__wakeup', 'getProductCollection'] + ['addFilters', '__wakeup'] ); $catalogSearchAdvanced->expects($this->once())->method('addFilters')->will( $this->returnCallback( @@ -49,8 +53,6 @@ function ($added) use (&$filters) { } ) ); - $catalogSearchAdvanced->expects($this->once())->method('getProductCollection') - ->will($this->returnValue($collection)); $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $context = $objectManager->getObject( @@ -189,20 +191,11 @@ public function testNoResultsHandle() $request = $this->createPartialMock(\Magento\Framework\App\Console\Request::class, ['getQueryValue']); $request->expects($this->once())->method('getQueryValue')->will($this->returnValue($expectedQuery)); - $collection = $this->createPartialMock( - \Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection::class, - ['getSize'] - ); - $collection->expects($this->once())->method('getSize')->will($this->returnValue(0)); - $catalogSearchAdvanced = $this->createPartialMock( \Magento\CatalogSearch\Model\Advanced::class, ['addFilters', '__wakeup', 'getProductCollection'] ); - $catalogSearchAdvanced->expects($this->once())->method('getProductCollection') - ->will($this->returnValue($collection)); - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $context = $objectManager->getObject( \Magento\Framework\App\Action\Context::class, diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/AdvancedTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/AdvancedTest.php index fc5915bb3cdff..a4f62ce83a1b8 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/AdvancedTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/AdvancedTest.php @@ -250,6 +250,7 @@ public function testAddFiltersVerifyAddConditionsToRegistry( 'productCollectionFactory' => $productCollectionFactory, 'storeManager' => $this->storeManager, 'currencyFactory' => $currencyFactory, + 'collectionProvider' => null ] ); $instance->addFilters($values); diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php index b76f51a132c94..fe29fa1ece011 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php @@ -7,7 +7,13 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; use Magento\CatalogSearch\Test\Unit\Model\ResourceModel\BaseCollection; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; /** * Tests Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection @@ -79,6 +85,42 @@ protected function setUp() $productLimitationFactoryMock->method('create') ->willReturn($productLimitationMock); + $searchCriteriaResolver = $this->getMockBuilder(SearchCriteriaResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['resolve']) + ->getMockForAbstractClass(); + $searchCriteriaResolverFactory = $this->getMockBuilder(SearchCriteriaResolverFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $searchCriteriaResolverFactory->expects($this->any()) + ->method('create') + ->willReturn($searchCriteriaResolver); + + $searchResultApplier = $this->getMockBuilder(SearchResultApplierInterface::class) + ->disableOriginalConstructor() + ->setMethods(['apply']) + ->getMockForAbstractClass(); + $searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $searchResultApplierFactory->expects($this->any()) + ->method('create') + ->willReturn($searchResultApplier); + + $totalRecordsResolver = $this->getMockBuilder(TotalRecordsResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['resolve']) + ->getMockForAbstractClass(); + $totalRecordsResolverFactory = $this->getMockBuilder(TotalRecordsResolverFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $totalRecordsResolverFactory->expects($this->any()) + ->method('create') + ->willReturn($totalRecordsResolver); + $this->advancedCollection = $this->objectManager->getObject( \Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection::class, [ @@ -90,6 +132,10 @@ protected function setUp() 'temporaryStorageFactory' => $this->temporaryStorageFactory, 'search' => $this->search, 'productLimitationFactory' => $productLimitationFactoryMock, + 'collectionProvider' => null, + 'searchCriteriaResolverFactory' => $searchCriteriaResolverFactory, + 'searchResultApplierFactory' => $searchResultApplierFactory, + 'totalRecordsResolverFactory' => $totalRecordsResolverFactory ] ); } @@ -117,18 +163,8 @@ public function testLike() ->willReturn($this->filterBuilder); $filter = $this->createMock(\Magento\Framework\Api\Filter::class); - $this->filterBuilder->expects($this->once())->method('create')->willReturn($filter); - - $criteria = $this->createMock(\Magento\Framework\Api\Search\SearchCriteria::class); - $this->criteriaBuilder->expects($this->once())->method('create')->willReturn($criteria); - $criteria->expects($this->once()) - ->method('setRequestName') - ->with('advanced_search_container'); - - $tempTable = $this->createMock(\Magento\Framework\DB\Ddl\Table::class); - $temporaryStorage = $this->createMock(\Magento\Framework\Search\Adapter\Mysql\TemporaryStorage::class); - $temporaryStorage->expects($this->once())->method('storeApiDocuments')->willReturn($tempTable); - $this->temporaryStorageFactory->expects($this->once())->method('create')->willReturn($temporaryStorage); + $this->filterBuilder->expects($this->any())->method('create')->willReturn($filter); + $searchResult = $this->createMock(\Magento\Framework\Api\Search\SearchResultInterface::class); $this->search->expects($this->once())->method('search')->willReturn($searchResult); diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php index 82490ab6b6d8b..f1c2161a052e0 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php @@ -5,6 +5,12 @@ */ namespace Magento\CatalogSearch\Test\Unit\Model\ResourceModel\Fulltext; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; use Magento\CatalogSearch\Test\Unit\Model\ResourceModel\BaseCollection; use Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory; use PHPUnit_Framework_MockObject_MockObject as MockObject; @@ -97,6 +103,41 @@ protected function setUp() $temporaryStorageFactory->expects($this->any()) ->method('create') ->willReturn($this->temporaryStorage); + $searchCriteriaResolver = $this->getMockBuilder(SearchCriteriaResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['resolve']) + ->getMockForAbstractClass(); + $searchCriteriaResolverFactory = $this->getMockBuilder(SearchCriteriaResolverFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $searchCriteriaResolverFactory->expects($this->any()) + ->method('create') + ->willReturn($searchCriteriaResolver); + + $searchResultApplier = $this->getMockBuilder(SearchResultApplierInterface::class) + ->disableOriginalConstructor() + ->setMethods(['apply']) + ->getMockForAbstractClass(); + $searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $searchResultApplierFactory->expects($this->any()) + ->method('create') + ->willReturn($searchResultApplier); + + $totalRecordsResolver = $this->getMockBuilder(TotalRecordsResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['resolve']) + ->getMockForAbstractClass(); + $totalRecordsResolverFactory = $this->getMockBuilder(TotalRecordsResolverFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $totalRecordsResolverFactory->expects($this->any()) + ->method('create') + ->willReturn($totalRecordsResolver); $this->model = $this->objectManager->getObject( \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection::class, @@ -106,6 +147,9 @@ protected function setUp() 'scopeConfig' => $this->scopeConfig, 'temporaryStorageFactory' => $temporaryStorageFactory, 'productLimitationFactory' => $productLimitationFactoryMock, + 'searchCriteriaResolverFactory' => $searchCriteriaResolverFactory, + 'searchResultApplierFactory' => $searchResultApplierFactory, + 'totalRecordsResolverFactory' => $totalRecordsResolverFactory, ] ); @@ -124,37 +168,10 @@ protected function tearDown() $reflectionProperty->setValue(null); } - /** - * @expectedException \Exception - * @expectedExceptionCode 333 - * @expectedExceptionMessage setRequestName - */ - public function testGetFacetedDataWithException() - { - $criteria = $this->createMock(\Magento\Framework\Api\Search\SearchCriteria::class); - $this->criteriaBuilder->expects($this->once())->method('create')->willReturn($criteria); - $criteria->expects($this->once()) - ->method('setRequestName') - ->withConsecutive(['catalog_view_container']) - ->willThrowException(new \Exception('setRequestName', 333)); - $this->model->getFacetedData('field'); - } - public function testGetFacetedDataWithEmptyAggregations() { - $criteria = $this->createMock(\Magento\Framework\Api\Search\SearchCriteria::class); - $this->criteriaBuilder->expects($this->once())->method('create')->willReturn($criteria); - $criteria->expects($this->once()) - ->method('setRequestName') - ->withConsecutive(['catalog_view_container']); $searchResult = $this->getMockBuilder(\Magento\Framework\Api\Search\SearchResultInterface::class) ->getMockForAbstractClass(); - $table = $this->getMockBuilder(\Magento\Framework\DB\Ddl\Table::class) - ->setMethods(['getName']) - ->getMock(); - $this->temporaryStorage->expects($this->once()) - ->method('storeApiDocuments') - ->willReturn($table); $this->search->expects($this->once()) ->method('search') ->willReturn($searchResult); diff --git a/app/code/Magento/CatalogSearch/etc/di.xml b/app/code/Magento/CatalogSearch/etc/di.xml index cc07384d4c525..5c80c92141727 100644 --- a/app/code/Magento/CatalogSearch/etc/di.xml +++ b/app/code/Magento/CatalogSearch/etc/di.xml @@ -14,6 +14,10 @@ <preference for="Magento\CatalogSearch\Model\Search\FilterMapper\FilterStrategyInterface" type="Magento\CatalogSearch\Model\Search\FilterMapper\FilterContext"/> <preference for="Magento\CatalogSearch\Model\Indexer\IndexSwitcherInterface" type="Magento\CatalogSearch\Model\Indexer\IndexSwitcherProxy"/> <preference for="Magento\CatalogSearch\Model\Adapter\Aggregation\RequestCheckerInterface" type="Magento\CatalogSearch\Model\Adapter\Aggregation\RequestCheckerComposite"/> + <preference for="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolver"/> + <preference for="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplier"/> + <preference for="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolver"/> + <preference for="Magento\CatalogSearch\Model\Search\ItemCollectionProviderInterface" type="Magento\CatalogSearch\Model\Search\ItemCollectionProvider"/> <type name="Magento\CatalogSearch\Model\Indexer\IndexerHandlerFactory"> <arguments> <argument name="handlers" xsi:type="array"> @@ -176,8 +180,13 @@ <argument name="collectionFactory" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Fulltext\SearchCollectionFactory</argument> </arguments> </virtualType> - <virtualType name="Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory" - type="Magento\Catalog\Model\ResourceModel\Product\CollectionFactory"> + + <virtualType name="Magento\CatalogSearch\Model\Advanced\ItemCollectionProvider" type="Magento\Catalog\Model\Layer\Search\ItemCollectionProvider"> + <arguments> + <argument name="collectionFactory" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory</argument> + </arguments> + </virtualType> + <virtualType name="Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory" type="Magento\Catalog\Model\ResourceModel\Product\CollectionFactory"> <arguments> <argument name="instanceName" xsi:type="string">Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection</argument> </arguments> @@ -185,6 +194,21 @@ <type name="Magento\CatalogSearch\Model\Advanced"> <arguments> <argument name="productCollectionFactory" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory</argument> + <argument name="collectionProvider" xsi:type="object">Magento\CatalogSearch\Model\Search\ItemCollectionProviderInterface</argument> + </arguments> + </type> + <type name="Magento\CatalogSearch\Model\Search\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="mysql" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory</item> + </argument> + </arguments> + </type> + <type name="Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyProvider"> + <arguments> + <argument name="strategies" xsi:type="array"> + <item name="mysql" xsi:type="object">Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategy</item> + </argument> </arguments> </type> <virtualType name="Magento\CatalogSearch\Model\Layer\Category\Context" type="Magento\Catalog\Model\Layer\Category\Context"> diff --git a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml index 83808df5b95e4..3f616ab791dfe 100644 --- a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml +++ b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml @@ -11,6 +11,8 @@ /** * @var $block \Magento\CatalogSearch\Block\Advanced\Result */ +/** this changes need for valid apply filters and configuration before search process is started */ +$productList = $block->getProductListHtml(); ?> <?php if ($results = $block->getResultCount()): ?> <div class="search found"> @@ -49,6 +51,6 @@ </div> <?php endif; ?> <?php if ($block->getResultCount()): ?> - <div class="search results"><?= $block->getProductListHtml() ?></div> + <div class="search results"><?= /* @escapeNotVerified */ $productList ?></div> <?php endif; ?> <?php $block->getSearchCriterias(); ?> diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php index 9aaa384776855..7b60c85049767 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php @@ -263,6 +263,7 @@ protected function _populateForUrlGeneration($rowData) if ($this->isGlobalScope($product->getStoreId())) { $this->populateGlobalProduct($product); } else { + $this->storesCache[$product->getStoreId()] = true; $this->addProductToImport($product, $product->getStoreId()); } return $this; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js index 6f9a1a46826da..d68b0682eb511 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js @@ -202,6 +202,13 @@ function ( } }, + /** + * Manage cancel button visibility + */ + canUseCancelBillingAddress: ko.computed(function () { + return quote.billingAddress() || lastSelectedBillingAddress; + }), + /** * Restore billing address */ diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html index 5f735fbb4daa9..63edb5057b933 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html @@ -22,7 +22,7 @@ <button class="action action-update" type="button" data-bind="click: updateAddress"> <span data-bind="i18n: 'Update'"></span> </button> - <button class="action action-cancel" type="button" data-bind="click: cancelAddressEdit"> + <button class="action action-cancel" type="button" data-bind="click: cancelAddressEdit, visible: canUseCancelBillingAddress()"> <span data-bind="i18n: 'Cancel'"></span> </button> </div> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml index 65fabfe25e817..2c351a12af72e 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml @@ -11,13 +11,12 @@ <test name="StoreViewLanguageCorrectSwitchTest"> <annotations> <features value="Cms"/> - <stories value="Store View (language) switch leads to 404"/> - <group value="Cms"/> <title value="Check that Store View(language) switches correct"/> <description value="Check that Store View(language) switches correct"/> <severity value="MAJOR"/> <testCaseId value="MAGETWO-96388"/> <useCaseId value="MAGETWO-57337"/> + <group value="Cms"/> </annotations> <before> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> @@ -37,7 +36,7 @@ <!-- Create StoreView --> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> - <argument name="customStore" value="NewStoreViewData"/> + <argument name="customStore" value="NewStoreViewData"/> </actionGroup> <!-- Add StoreView To Cms Page--> @@ -51,10 +50,18 @@ <see userInput="$$createFirstCmsPage.title$$" stepKey="seePageTitle"/> <!-- Switch StoreView and check that Cms Page is open --> - <click selector="{{StorefrontHeaderSection.storeViewSwitcher}}" stepKey="clickStoreViewSwitcher"/> - <waitForElementVisible selector="{{StorefrontHeaderSection.storeViewDropdown}}" stepKey="waitForStoreViewDropDown"/> - <click selector="{{StorefrontHeaderSection.storeViewOption(NewStoreViewData.code)}}" stepKey="selectStoreView"/> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchToCustomStoreView"> + <argument name="storeView" value="NewStoreViewData"/> + </actionGroup> <amOnPage url="{{StorefrontHomePage.url}}/$$createSecondCmsPage.identifier$$" stepKey="gotToSecondCmsPage"/> <see userInput="$$createSecondCmsPage.title$$" stepKey="seePageTitle1"/> + + <!--Open first Cms page on custom store view--> + <amOnPage url="{{StorefrontHomePage.url}}/$$createFirstCmsPage.identifier$$" stepKey="gotToFirstCmsPage1"/> + <see userInput="Whoops, our bad..." stepKey="seePageError"/> + + <!--Switch to default store view and check Cms page--> + <actionGroup ref="StorefrontSwitchDefaultStoreViewActionGroup" stepKey="switchToDefualtStoreView"/> + <see userInput="$$createFirstCmsPage.title$$" stepKey="seePageTitle2"/> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php index bee334596e990..f2bf3116af9e4 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php @@ -7,14 +7,15 @@ */ namespace Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Catalog\Model\Product; + +/** + * Class Price for configurable product + */ class Price extends \Magento\Catalog\Model\Product\Type\Price { /** - * Get product final price - * - * @param float $qty - * @param \Magento\Catalog\Model\Product $product - * @return float + * @inheritdoc */ public function getFinalPrice($qty, $product) { @@ -22,7 +23,10 @@ public function getFinalPrice($qty, $product) return $product->getCalculatedFinalPrice(); } if ($product->getCustomOption('simple_product') && $product->getCustomOption('simple_product')->getProduct()) { - $finalPrice = parent::getFinalPrice($qty, $product->getCustomOption('simple_product')->getProduct()); + /** @var Product $simpleProduct */ + $simpleProduct = $product->getCustomOption('simple_product')->getProduct(); + $simpleProduct->setCustomerGroupId($product->getCustomerGroupId()); + $finalPrice = parent::getFinalPrice($qty, $simpleProduct); } else { $priceInfo = $product->getPriceInfo(); $finalPrice = $priceInfo->getPrice('final_price')->getAmount()->getValue(); @@ -35,7 +39,7 @@ public function getFinalPrice($qty, $product) } /** - * {@inheritdoc} + * @inheritdoc */ public function getPrice($product) { @@ -48,6 +52,7 @@ public function getPrice($product) } } } + return 0; } } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php index 64b9b3776442a..ab52d4eb86021 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php @@ -6,22 +6,47 @@ namespace Magento\ConfigurableProduct\Test\Unit\Model\Product\Type\Configurable; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Configuration\Item\Option; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price as ConfigurablePrice; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\Price\PriceInterface; +use Magento\Framework\Pricing\PriceInfo\Base as PriceInfoBase; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use PHPUnit\Framework\MockObject\MockObject; class PriceTest extends \PHPUnit\Framework\TestCase { - /** @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price */ + /** + * @var ObjectManagerHelper + */ + protected $objectManagerHelper; + + /** + * @var ConfigurablePrice + */ protected $model; - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; + /** + * @var ManagerInterface|MockObject + */ + private $eventManagerMock; + /** + * @inheritdoc + */ protected function setUp() { $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->eventManagerMock = $this->createPartialMock( + ManagerInterface::class, + ['dispatch'] + ); $this->model = $this->objectManagerHelper->getObject( - \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price::class + ConfigurablePrice::class, + ['eventManager' => $this->eventManagerMock] ); } @@ -29,29 +54,29 @@ public function testGetFinalPrice() { $finalPrice = 10; $qty = 1; - $configurableProduct = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->disableOriginalConstructor() - ->setMethods(['getCustomOption', 'getPriceInfo', 'setFinalPrice', '__wakeUp']) - ->getMock(); - $customOption = $this->getMockBuilder(\Magento\Catalog\Model\Product\Configuration\Item\Option::class) + + /** @var Product|MockObject $configurableProduct */ + $configurableProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() - ->setMethods(['getProduct']) + ->setMethods(['getCustomOption', 'getPriceInfo', 'setFinalPrice']) ->getMock(); - $priceInfo = $this->getMockBuilder(\Magento\Framework\Pricing\PriceInfo\Base::class) + /** @var PriceInfoBase|MockObject $priceInfo */ + $priceInfo = $this->getMockBuilder(PriceInfoBase::class) ->disableOriginalConstructor() ->setMethods(['getPrice']) ->getMock(); - $price = $this->getMockBuilder(\Magento\Framework\Pricing\Price\PriceInterface::class) + /** @var PriceInterface|MockObject $price */ + $price = $this->getMockBuilder(PriceInterface::class) ->disableOriginalConstructor() ->getMock(); - $amount = $this->getMockBuilder(\Magento\Framework\Pricing\Amount\AmountInterface::class) + /** @var AmountInterface|MockObject $amount */ + $amount = $this->getMockBuilder(AmountInterface::class) ->disableOriginalConstructor() ->getMock(); $configurableProduct->expects($this->any()) ->method('getCustomOption') ->willReturnMap([['simple_product', false], ['option_ids', false]]); - $customOption->expects($this->never())->method('getProduct'); $configurableProduct->expects($this->once())->method('getPriceInfo')->willReturn($priceInfo); $priceInfo->expects($this->once())->method('getPrice')->with('final_price')->willReturn($price); $price->expects($this->once())->method('getAmount')->willReturn($amount); @@ -60,4 +85,60 @@ public function testGetFinalPrice() $this->assertEquals($finalPrice, $this->model->getFinalPrice($qty, $configurableProduct)); } + + public function testGetFinalPriceWithSimpleProduct() + { + $finalPrice = 10; + $qty = 1; + $customerGroupId = 1; + + /** @var Product|MockObject $configurableProduct */ + $configurableProduct = $this->createPartialMock( + Product::class, + ['getCustomOption', 'setFinalPrice', 'getCustomerGroupId'] + ); + /** @var Option|MockObject $customOption */ + $customOption = $this->createPartialMock( + Option::class, + ['getProduct'] + ); + /** @var Product|MockObject $simpleProduct */ + $simpleProduct = $this->createPartialMock( + Product::class, + ['setCustomerGroupId', 'setFinalPrice', 'getPrice', 'getTierPrice', 'getData', 'getCustomOption'] + ); + + $configurableProduct->method('getCustomOption') + ->willReturnMap([ + ['simple_product', $customOption], + ['option_ids', false] + ]); + $configurableProduct->method('getCustomerGroupId')->willReturn($customerGroupId); + $configurableProduct->expects($this->atLeastOnce()) + ->method('setFinalPrice') + ->with($finalPrice) + ->willReturnSelf(); + $customOption->method('getProduct')->willReturn($simpleProduct); + $simpleProduct->expects($this->atLeastOnce()) + ->method('setCustomerGroupId') + ->with($customerGroupId) + ->willReturnSelf(); + $simpleProduct->method('getPrice')->willReturn($finalPrice); + $simpleProduct->method('getTierPrice')->with($qty)->willReturn($finalPrice); + $simpleProduct->expects($this->atLeastOnce()) + ->method('setFinalPrice') + ->with($finalPrice) + ->willReturnSelf(); + $simpleProduct->method('getData')->with('final_price')->willReturn($finalPrice); + $simpleProduct->method('getCustomOption')->with('option_ids')->willReturn(false); + $this->eventManagerMock->expects($this->once()) + ->method('dispatch') + ->with('catalog_product_get_final_price', ['product' => $simpleProduct, 'qty' => $qty]); + + $this->assertEquals( + $finalPrice, + $this->model->getFinalPrice($qty, $configurableProduct), + 'The final price calculation is wrong' + ); + } } diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php index b7f21696162dd..26173fcf29b0c 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php @@ -19,13 +19,19 @@ class Converter implements ConverterInterface */ private const ES_NO_INDEX = false; + /** + * Text flags for Elasticsearch no analyze index value + */ + private const ES_NO_ANALYZE = false; + /** * Mapping between internal data types and elastic service. * * @var array */ private $mapping = [ - 'no_index' => self::ES_NO_INDEX, + ConverterInterface::INTERNAL_NO_INDEX_VALUE => self::ES_NO_INDEX, + ConverterInterface::INTERNAL_NO_ANALYZE_VALUE => self::ES_NO_ANALYZE, ]; /** diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Adapter.php b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Adapter.php index a6838d831b4bc..0ae347d5791ad 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Adapter.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Adapter.php @@ -125,6 +125,7 @@ public function query(RequestInterface $request) [ 'documents' => $rawDocuments, 'aggregations' => $aggregationBuilder->build($request, $rawResponse), + 'total' => isset($rawResponse['hits']['total']) ? $rawResponse['hits']['total'] : 0 ] ); return $queryResponse; diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php index db961d86962e9..09968db00aa25 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Query; +use Magento\Elasticsearch\SearchAdapter\Query\Builder\Sort; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Search\RequestInterface; use Magento\Elasticsearch\Model\Config; use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; @@ -12,6 +15,8 @@ use Magento\Framework\App\ScopeResolverInterface; /** + * Query builder for search adapter. + * * @api * @since 100.1.0 */ @@ -41,22 +46,30 @@ class Builder */ protected $scopeResolver; + /** + * @var Sort + */ + protected $sortBuilder; + /** * @param Config $clientConfig * @param SearchIndexNameResolver $searchIndexNameResolver * @param AggregationBuilder $aggregationBuilder * @param ScopeResolverInterface $scopeResolver + * @param Sort|null $sortBuilder */ public function __construct( Config $clientConfig, SearchIndexNameResolver $searchIndexNameResolver, AggregationBuilder $aggregationBuilder, - ScopeResolverInterface $scopeResolver + ScopeResolverInterface $scopeResolver, + Sort $sortBuilder = null ) { $this->clientConfig = $clientConfig; $this->searchIndexNameResolver = $searchIndexNameResolver; $this->aggregationBuilder = $aggregationBuilder; $this->scopeResolver = $scopeResolver; + $this->sortBuilder = $sortBuilder ?: ObjectManager::getInstance()->get(Sort::class); } /** @@ -70,6 +83,7 @@ public function initQuery(RequestInterface $request) { $dimension = current($request->getDimensions()); $storeId = $this->scopeResolver->getScope($dimension->getValue())->getId(); + $searchQuery = [ 'index' => $this->searchIndexNameResolver->getIndexName($storeId, $request->getIndex()), 'type' => $this->clientConfig->getEntityType(), @@ -77,6 +91,7 @@ public function initQuery(RequestInterface $request) 'from' => $request->getFrom(), 'size' => $request->getSize(), 'stored_fields' => ['_id', '_score'], + 'sort' => $this->sortBuilder->getSort($request), 'query' => [], ], ]; diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php index 17ebf9c83903e..54586fa357ff0 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php @@ -146,6 +146,16 @@ public function getAttributeCode(): string return $this->attributeCode; } + /** + * Check if attribute is sortable. + * + * @return bool + */ + public function isSortable(): bool + { + return (int)$this->getAttribute()->getUsedForSortBy() === 1; + } + /** * Check if attribute is defined by user. * diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter/DummyAttribute.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter/DummyAttribute.php index b8c0da53c53e0..19b9f85c44b03 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter/DummyAttribute.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter/DummyAttribute.php @@ -59,4 +59,24 @@ public function setCustomAttributes(array $attributes) { return $this; } + + /** + * Get property value that guarantee of using an attribute in sort purposes on the storefront. + * + * @return bool + */ + public function getUsedForSortBy() + { + return false; + } + + /** + * Dummy attribute doesn't have backend type. + * + * @return null + */ + public function getBackendType() + { + return null; + } } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php index c7e2a4beabb5c..181cbd4dfd4b3 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php @@ -18,6 +18,8 @@ use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface as FieldNameResolver; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Framework\App\ObjectManager; /** * Provide dynamic fields for product. @@ -27,10 +29,18 @@ class DynamicField implements FieldProviderInterface /** * Category list. * + * @deprecated * @var CategoryListInterface */ private $categoryList; + /** + * Category collection. + * + * @var Collection + */ + private $categoryCollection; + /** * Customer group repository. * @@ -73,6 +83,7 @@ class DynamicField implements FieldProviderInterface * @param CategoryListInterface $categoryList * @param FieldNameResolver $fieldNameResolver * @param AttributeProvider $attributeAdapterProvider + * @param Collection|null $categoryCollection */ public function __construct( FieldTypeConverterInterface $fieldTypeConverter, @@ -81,7 +92,8 @@ public function __construct( SearchCriteriaBuilder $searchCriteriaBuilder, CategoryListInterface $categoryList, FieldNameResolver $fieldNameResolver, - AttributeProvider $attributeAdapterProvider + AttributeProvider $attributeAdapterProvider, + Collection $categoryCollection = null ) { $this->groupRepository = $groupRepository; $this->searchCriteriaBuilder = $searchCriteriaBuilder; @@ -90,6 +102,8 @@ public function __construct( $this->categoryList = $categoryList; $this->fieldNameResolver = $fieldNameResolver; $this->attributeAdapterProvider = $attributeAdapterProvider; + $this->categoryCollection = $categoryCollection ?: + ObjectManager::getInstance()->get(Collection::class); } /** @@ -98,18 +112,17 @@ public function __construct( public function getFields(array $context = []): array { $allAttributes = []; - $searchCriteria = $this->searchCriteriaBuilder->create(); - $categories = $this->categoryList->getList($searchCriteria)->getItems(); + $categoryIds = $this->categoryCollection->getAllIds(); $positionAttribute = $this->attributeAdapterProvider->getByAttributeCode('position'); $categoryNameAttribute = $this->attributeAdapterProvider->getByAttributeCode('category_name'); - foreach ($categories as $category) { + foreach ($categoryIds as $categoryId) { $categoryPositionKey = $this->fieldNameResolver->getFieldName( $positionAttribute, - ['categoryId' => $category->getId()] + ['categoryId' => $categoryId] ); $categoryNameKey = $this->fieldNameResolver->getFieldName( $categoryNameAttribute, - ['categoryId' => $category->getId()] + ['categoryId' => $categoryId] ); $allAttributes[$categoryPositionKey] = [ 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING), @@ -121,6 +134,7 @@ public function getFields(array $context = []): array ]; } + $searchCriteria = $this->searchCriteriaBuilder->create(); $groups = $this->groupRepository->getList($searchCriteria)->getItems(); $priceAttribute = $this->attributeAdapterProvider->getByAttributeCode('price'); foreach ($groups as $group) { diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php index 5abe4972e34d0..535ab62dd5991 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php @@ -17,13 +17,19 @@ class Converter implements ConverterInterface */ private const ES_NO_INDEX = 'no'; + /** + * Text flags for Elasticsearch no analyze index value + */ + private const ES_NO_ANALYZE = 'not_analyzed'; + /** * Mapping between internal data types and elastic service. * * @var array */ private $mapping = [ - 'no_index' => self::ES_NO_INDEX, + ConverterInterface::INTERNAL_NO_INDEX_VALUE => self::ES_NO_INDEX, + ConverterInterface::INTERNAL_NO_ANALYZE_VALUE => self::ES_NO_ANALYZE, ]; /** diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/ConverterInterface.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/ConverterInterface.php index 02c4d29558dad..5ecfd62430032 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/ConverterInterface.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/ConverterInterface.php @@ -17,6 +17,7 @@ interface ConverterInterface */ public const INTERNAL_NO_INDEX_VALUE = 'no_index'; public const INTERNAL_INDEX_VALUE = 'index'; + public const INTERNAL_NO_ANALYZE_VALUE = 'no_analyze'; /** * Get service field index type. diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php index 3208ca7fc6171..5f319daeb3b39 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php @@ -51,22 +51,22 @@ public function __construct( */ public function getFieldName(AttributeAdapter $attribute, $context = []): ?string { - $fieldType = $this->fieldTypeResolver->getFieldType($attribute); $attributeCode = $attribute->getAttributeCode(); - $frontendInput = $attribute->getFrontendInput(); if (empty($context['type'])) { - $fieldName = $attributeCode; - } elseif ($context['type'] === FieldMapperInterface::TYPE_FILTER) { + return $attributeCode; + } + + $fieldType = $this->fieldTypeResolver->getFieldType($attribute); + $frontendInput = $attribute->getFrontendInput(); + $fieldName = $attributeCode; + if ($context['type'] === FieldMapperInterface::TYPE_FILTER) { if ($this->isStringServiceFieldType($fieldType)) { - return $this->getFieldName( - $attribute, - array_merge($context, ['type' => FieldMapperInterface::TYPE_QUERY]) - ); + return $this->getQueryTypeFieldName($frontendInput, $fieldType, $attributeCode); } $fieldName = $attributeCode; } elseif ($context['type'] === FieldMapperInterface::TYPE_QUERY) { $fieldName = $this->getQueryTypeFieldName($frontendInput, $fieldType, $attributeCode); - } else { + } elseif ($context['type'] == FieldMapperInterface::TYPE_SORT && $attribute->isSortable()) { $fieldName = 'sort_' . $attributeCode; } @@ -115,10 +115,11 @@ private function getQueryTypeFieldName($frontendInput, $fieldType, $attributeCod private function getRefinedFieldName($frontendInput, $fieldType, $attributeCode) { $stringTypeKey = $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING); + $keywordTypeKey = $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD); switch ($frontendInput) { case 'select': case 'multiselect': - return in_array($fieldType, [$stringTypeKey, 'integer'], true) + return in_array($fieldType, [$stringTypeKey, $keywordTypeKey, 'integer'], true) ? $attributeCode . '_value' : $attributeCode; case 'boolean': diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/SpecialAttribute.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/SpecialAttribute.php index 4fa16db98639e..652fc853adbcc 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/SpecialAttribute.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/SpecialAttribute.php @@ -24,7 +24,9 @@ class SpecialAttribute implements ResolverInterface */ public function getFieldName(AttributeAdapter $attribute, $context = []): ?string { - if (in_array($attribute->getAttributeCode(), ['id', 'sku', 'store_id', 'visibility'], true)) { + if (in_array($attribute->getAttributeCode(), ['id', 'sku', 'store_id', 'visibility'], true) + && empty($context['type']) + ) { return $attribute->getAttributeCode(); } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php index b5c78cbc28f45..6876b23bbb156 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php @@ -7,6 +7,7 @@ namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider; +use Magento\Framework\App\ObjectManager; use Magento\Eav\Model\Config; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; @@ -19,6 +20,7 @@ as FieldTypeResolver; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ResolverInterface as FieldIndexResolver; +use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; /** * Provide static fields for mapping of product. @@ -55,6 +57,11 @@ class StaticField implements FieldProviderInterface */ private $fieldIndexResolver; + /** + * @var FieldName\ResolverInterface + */ + private $fieldNameResolver; + /** * @param Config $eavConfig * @param FieldTypeConverterInterface $fieldTypeConverter @@ -62,6 +69,7 @@ class StaticField implements FieldProviderInterface * @param FieldTypeResolver $fieldTypeResolver * @param FieldIndexResolver $fieldIndexResolver * @param AttributeProvider $attributeAdapterProvider + * @param FieldName\ResolverInterface|null $fieldNameResolver */ public function __construct( Config $eavConfig, @@ -69,7 +77,8 @@ public function __construct( IndexTypeConverterInterface $indexTypeConverter, FieldTypeResolver $fieldTypeResolver, FieldIndexResolver $fieldIndexResolver, - AttributeProvider $attributeAdapterProvider + AttributeProvider $attributeAdapterProvider, + FieldName\ResolverInterface $fieldNameResolver = null ) { $this->eavConfig = $eavConfig; $this->fieldTypeConverter = $fieldTypeConverter; @@ -77,6 +86,8 @@ public function __construct( $this->fieldTypeResolver = $fieldTypeResolver; $this->fieldIndexResolver = $fieldIndexResolver; $this->attributeAdapterProvider = $attributeAdapterProvider; + $this->fieldNameResolver = $fieldNameResolver ?: ObjectManager::getInstance() + ->get(FieldName\ResolverInterface::class); } /** @@ -84,6 +95,7 @@ public function __construct( * * @param array $context * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getFields(array $context = []): array { @@ -92,19 +104,38 @@ public function getFields(array $context = []): array foreach ($attributes as $attribute) { $attributeAdapter = $this->attributeAdapterProvider->getByAttributeCode($attribute->getAttributeCode()); - $code = $attributeAdapter->getAttributeCode(); + $fieldName = $this->fieldNameResolver->getFieldName($attributeAdapter); - $allAttributes[$code] = [ + $allAttributes[$fieldName] = [ 'type' => $this->fieldTypeResolver->getFieldType($attributeAdapter), ]; $index = $this->fieldIndexResolver->getFieldIndex($attributeAdapter); if (null !== $index) { - $allAttributes[$code]['index'] = $index; + $allAttributes[$fieldName]['index'] = $index; + } + + if ($attributeAdapter->isSortable()) { + $sortFieldName = $this->fieldNameResolver->getFieldName( + $attributeAdapter, + ['type' => FieldMapperInterface::TYPE_SORT] + ); + $allAttributes[$fieldName]['fields'][$sortFieldName] = [ + 'type' => $this->fieldTypeConverter->convert( + FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD + ), + 'index' => $this->indexTypeConverter->convert( + IndexTypeConverterInterface::INTERNAL_NO_ANALYZE_VALUE + ) + ]; } if ($attributeAdapter->isComplexType()) { - $allAttributes[$code . '_value'] = [ + $childFieldName = $this->fieldNameResolver->getFieldName( + $attributeAdapter, + ['type' => FieldMapperInterface::TYPE_QUERY] + ); + $allAttributes[$childFieldName] = [ 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING) ]; } diff --git a/app/code/Magento/Elasticsearch/Model/Advanced/ProductCollectionPrepareStrategy.php b/app/code/Magento/Elasticsearch/Model/Advanced/ProductCollectionPrepareStrategy.php new file mode 100644 index 0000000000000..b3f8a56110f8d --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Advanced/ProductCollectionPrepareStrategy.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Elasticsearch\Model\Advanced; + +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\Config; +use Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyInterface; + +/** + * Strategy interface for preparing product collection. + */ +class ProductCollectionPrepareStrategy implements ProductCollectionPrepareStrategyInterface +{ + /** + * @var Config + */ + private $catalogConfig; + + /** + * @param Config $catalogConfig + */ + public function __construct( + Config $catalogConfig + ) { + $this->catalogConfig = $catalogConfig; + } + + /** + * @inheritdoc + */ + public function prepare(Collection $collection) + { + $collection + ->addAttributeToSelect($this->catalogConfig->getProductAttributes()) + ->addMinimalPrice() + ->addTaxPercents(); + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Layer/Category/ItemCollectionProvider.php b/app/code/Magento/Elasticsearch/Model/Layer/Category/ItemCollectionProvider.php new file mode 100644 index 0000000000000..ef2992e1fff9f --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Layer/Category/ItemCollectionProvider.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\Model\Layer\Category; + +use Magento\Catalog\Model\Layer\ItemCollectionProviderInterface; +use Magento\Framework\Search\EngineResolverInterface; + +/** + * Catalog search category layer collection provider. + */ +class ItemCollectionProvider implements ItemCollectionProviderInterface +{ + /** + * @var EngineResolverInterface + */ + private $engineResolver; + + /** + * @var array + */ + private $factories; + + /** + * ItemCollectionProvider constructor. + * @param EngineResolverInterface $engineResolver + * @param array $factories + */ + public function __construct( + EngineResolverInterface $engineResolver, + array $factories + ) { + $this->engineResolver = $engineResolver; + $this->factories = $factories; + } + + /** + * @inheritdoc + */ + public function getCollection(\Magento\Catalog\Model\Category $category) + { + if (!isset($this->factories[$this->engineResolver->getCurrentSearchEngine()])) { + throw new \DomainException('Undefined factory ' . $this->engineResolver->getCurrentSearchEngine()); + } + $collection = $this->factories[$this->engineResolver->getCurrentSearchEngine()]->create(); + $collection->addCategoryFilter($category); + + return $collection; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Layer/Search/ItemCollectionProvider.php b/app/code/Magento/Elasticsearch/Model/Layer/Search/ItemCollectionProvider.php new file mode 100644 index 0000000000000..7d2a30b493d30 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Layer/Search/ItemCollectionProvider.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\Model\Layer\Search; + +use Magento\Catalog\Model\Layer\ItemCollectionProviderInterface; +use Magento\Framework\Search\EngineResolverInterface; + +/** + * Catalog search category layer collection provider. + */ +class ItemCollectionProvider implements ItemCollectionProviderInterface +{ + /** + * @var EngineResolverInterface + */ + private $engineResolver; + + /** + * @var array + */ + private $factories; + + /** + * ItemCollectionProvider constructor. + * @param EngineResolverInterface $engineResolver + * @param array $factories + */ + public function __construct( + EngineResolverInterface $engineResolver, + array $factories + ) { + $this->engineResolver = $engineResolver; + $this->factories = $factories; + } + + /** + * @inheritdoc + */ + public function getCollection(\Magento\Catalog\Model\Category $category) + { + if (!isset($this->factories[$this->engineResolver->getCurrentSearchEngine()])) { + throw new \DomainException('Undefined factory ' . $this->engineResolver->getCurrentSearchEngine()); + } + $collection = $this->factories[$this->engineResolver->getCurrentSearchEngine()]->create(); + + return $collection; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php new file mode 100644 index 0000000000000..255c7885e84b9 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection; + +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; +use Magento\Framework\Data\Collection; +use Magento\Framework\Api\Search\SearchCriteriaBuilder; +use Magento\Framework\Api\Search\SearchCriteria; + +/** + * Resolve specific attributes for search criteria. + */ +class SearchCriteriaResolver implements SearchCriteriaResolverInterface +{ + /** + * @var SearchCriteriaBuilder + */ + private $builder; + + /** + * @var Collection + */ + private $collection; + + /** + * @var string + */ + private $searchRequestName; + + /** + * @var int + */ + private $size; + + /** + * @var array + */ + private $orders; + + /** + * @var int + */ + private $currentPage; + + /** + * SearchCriteriaResolver constructor. + * @param SearchCriteriaBuilder $builder + * @param Collection $collection + * @param string $searchRequestName + * @param int $currentPage + * @param int $size + * @param array $orders + */ + public function __construct( + SearchCriteriaBuilder $builder, + Collection $collection, + string $searchRequestName, + int $currentPage, + int $size, + ?array $orders + ) { + $this->builder = $builder; + $this->collection = $collection; + $this->searchRequestName = $searchRequestName; + $this->currentPage = $currentPage; + $this->size = $size; + $this->orders = $orders; + } + + /** + * @inheritdoc + */ + public function resolve(): SearchCriteria + { + $this->builder->setPageSize($this->size); + $searchCriteria = $this->builder->create(); + $searchCriteria->setRequestName($this->searchRequestName); + $searchCriteria->setSortOrders($this->orders); + $searchCriteria->setCurrentPage($this->currentPage - 1); + + return $searchCriteria; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php new file mode 100644 index 0000000000000..3ae2d384782c3 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection; + +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; +use Magento\Framework\Data\Collection; +use Magento\Framework\Api\Search\SearchResultInterface; + +/** + * Resolve specific attributes for search criteria. + */ +class SearchResultApplier implements SearchResultApplierInterface +{ + /** + * @var Collection|\Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection + */ + private $collection; + + /** + * @var SearchResultInterface + */ + private $searchResult; + + /** + * @param Collection $collection + * @param SearchResultInterface $searchResult + */ + public function __construct( + Collection $collection, + SearchResultInterface $searchResult + ) { + $this->collection = $collection; + $this->searchResult = $searchResult; + } + + /** + * @inheritdoc + */ + public function apply() + { + if (empty($this->searchResult->getItems())) { + $this->collection->getSelect()->where('NULL'); + return; + } + $ids = []; + foreach ($this->searchResult->getItems() as $item) { + $ids[] = (int)$item->getId(); + } + $this->collection->setPageSize(null); + $this->collection->getSelect()->where('e.entity_id IN (?)', $ids); + $orderList = join(',', $ids); + $this->collection->getSelect()->reset(\Magento\Framework\DB\Select::ORDER); + $this->collection->getSelect()->order("FIELD(e.entity_id,$orderList)"); + } +} diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolver.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolver.php new file mode 100644 index 0000000000000..109721fcc71a9 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolver.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection; + +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; +use Magento\Framework\Api\Search\SearchResultInterface; + +/** + * Resolve total records count. + */ +class TotalRecordsResolver implements TotalRecordsResolverInterface +{ + /** + * @var SearchResultInterface + */ + private $searchResult; + + /** + * @param SearchResultInterface $searchResult + */ + public function __construct( + SearchResultInterface $searchResult + ) { + $this->searchResult = $searchResult; + } + + /** + * @inheritdoc + */ + public function resolve(): ?int + { + return $this->searchResult->getTotalCount(); + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Adapter.php b/app/code/Magento/Elasticsearch/SearchAdapter/Adapter.php index 43b2bfe553a98..6f9ef552351fd 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Adapter.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Adapter.php @@ -68,8 +68,7 @@ public function __construct( } /** - * @param RequestInterface $request - * @return QueryResponse + * @inheritdoc */ public function query(RequestInterface $request) { @@ -86,6 +85,7 @@ public function query(RequestInterface $request) [ 'documents' => $rawDocuments, 'aggregations' => $aggregationBuilder->build($request, $rawResponse), + 'total' => isset($rawResponse['hits']['total']) ? $rawResponse['hits']['total'] : 0 ] ); return $queryResponse; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder.php index cf75366b9b25e..d0aaa4b3dd572 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder.php @@ -3,19 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Elasticsearch\SearchAdapter\Query; use Magento\Framework\Search\RequestInterface; -use Magento\Framework\App\ScopeResolverInterface; use Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Query\Builder as Elasticsearch5Builder; /** + * Query builder for search adapter. + * * @api * @since 100.1.0 */ class Builder extends Elasticsearch5Builder { - /** * Set initial settings for query * @@ -34,6 +35,7 @@ public function initQuery(RequestInterface $request) 'from' => $request->getFrom(), 'size' => $request->getSize(), 'fields' => ['_id', '_score'], + 'sort' => $this->sortBuilder->getSort($request), 'query' => [], ], ]; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php new file mode 100644 index 0000000000000..5ccf202e3812b --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\SearchAdapter\Query\Builder; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface + as FieldNameResolver; +use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; +use Magento\Framework\Search\RequestInterface; + +/** + * Sort builder. + */ +class Sort +{ + /** + * List of fields that need to skipp by default. + */ + private const DEFAULT_SKIPPED_FIELDS = [ + 'entity_id', + ]; + + /** + * Default mapping for special fields. + */ + private const DEFAULT_MAP = [ + 'relevance' => '_score', + ]; + + /** + * @var AttributeProvider + */ + private $attributeAdapterProvider; + + /** + * @var FieldNameResolver + */ + private $fieldNameResolver; + + /** + * @var array + */ + private $skippedFields; + + /** + * @var array + */ + private $map; + + /** + * @param AttributeProvider $attributeAdapterProvider + * @param FieldNameResolver $fieldNameResolver + * @param array $skippedFields + * @param array $map + */ + public function __construct( + AttributeProvider $attributeAdapterProvider, + FieldNameResolver $fieldNameResolver, + array $skippedFields = [], + array $map = [] + ) { + $this->attributeAdapterProvider = $attributeAdapterProvider; + $this->fieldNameResolver = $fieldNameResolver; + $this->skippedFields = array_merge(self::DEFAULT_SKIPPED_FIELDS, $skippedFields); + $this->map = array_merge(self::DEFAULT_MAP, $map); + } + + /** + * Prepare sort. + * + * @param RequestInterface $request + * @return array + */ + public function getSort(RequestInterface $request) + { + $sorts = []; + foreach ($request->getSort() as $item) { + if (in_array($item['field'], $this->skippedFields)) { + continue; + } + $attribute = $this->attributeAdapterProvider->getByAttributeCode($item['field']); + $fieldName = $this->fieldNameResolver->getFieldName($attribute); + if (isset($this->map[$fieldName])) { + $fieldName = $this->map[$fieldName]; + } + if ($attribute->isSortable() && !($attribute->isFloatType() || $attribute->isIntegerType())) { + $suffix = $this->fieldNameResolver->getFieldName( + $attribute, + ['type' => FieldMapperInterface::TYPE_SORT] + ); + $fieldName .= '.' . $suffix; + } + $sorts[] = [ + $fieldName => [ + 'order' => strtolower($item['direction']) + ] + ]; + } + + return $sorts; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/ResponseFactory.php b/app/code/Magento/Elasticsearch/SearchAdapter/ResponseFactory.php index 33fda48f4af57..0813975ac9a4b 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/ResponseFactory.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/ResponseFactory.php @@ -76,6 +76,7 @@ public function create($response) [ 'documents' => $documents, 'aggregations' => $aggregations, + 'total' => $response['total'] ] ); } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php index ba5e97aa14b54..7c2a33c05aa08 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php @@ -24,6 +24,7 @@ use Magento\Customer\Api\Data\GroupInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface as FieldNameResolver; +use Magento\Catalog\Model\ResourceModel\Category\Collection; /** * @SuppressWarnings(PHPMD) @@ -65,6 +66,11 @@ class DynamicFieldTest extends \PHPUnit\Framework\TestCase */ private $categoryList; + /** + * @var Collection + */ + private $categoryCollection; + /** * @var FieldNameResolver */ @@ -100,6 +106,10 @@ protected function setUp() $this->categoryList = $this->getMockBuilder(CategoryListInterface::class) ->disableOriginalConstructor() ->getMock(); + $this->categoryCollection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->setMethods(['getAllIds']) + ->getMock(); $objectManager = new ObjectManagerHelper($this); @@ -113,6 +123,7 @@ protected function setUp() 'attributeAdapterProvider' => $this->attributeAdapterProvider, 'categoryList' => $this->categoryList, 'fieldNameResolver' => $this->fieldNameResolver, + 'categoryCollection' => $this->categoryCollection, ] ); } @@ -124,7 +135,6 @@ protected function setUp() * @param $groupId * @param array $expected * @return void - * @throws \Magento\Framework\Exception\LocalizedException */ public function testGetAllAttributesTypes( $complexType, @@ -138,10 +148,6 @@ public function testGetAllAttributesTypes( $this->searchCriteriaBuilder->expects($this->any()) ->method('create') ->willReturn($searchCriteria); - $categorySearchResults = $this->getMockBuilder(CategorySearchResultsInterface::class) - ->disableOriginalConstructor() - ->setMethods(['getItems']) - ->getMockForAbstractClass(); $groupSearchResults = $this->getMockBuilder(GroupSearchResultsInterface::class) ->disableOriginalConstructor() ->setMethods(['getItems']) @@ -156,19 +162,10 @@ public function testGetAllAttributesTypes( $groupSearchResults->expects($this->any()) ->method('getItems') ->willReturn([$group]); - $category = $this->getMockBuilder(CategoryInterface::class) - ->disableOriginalConstructor() - ->setMethods(['getId']) - ->getMockForAbstractClass(); - $category->expects($this->any()) - ->method('getId') - ->willReturn($categoryId); - $categorySearchResults->expects($this->any()) - ->method('getItems') - ->willReturn([$category]); - $this->categoryList->expects($this->any()) - ->method('getList') - ->willReturn($categorySearchResults); + + $this->categoryCollection->expects($this->any()) + ->method('getAllIds') + ->willReturn([$categoryId]); $categoryAttributeMock = $this->getMockBuilder(AttributeAdapter::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php index 4fa99f3bf834d..fd5c87bc9518b 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php @@ -66,6 +66,7 @@ protected function setUp() * @param $fieldType * @param $attributeCode * @param $frontendInput + * @param $isSortable * @param $context * @param $expected * @return void @@ -74,6 +75,7 @@ public function testGetFieldName( $fieldType, $attributeCode, $frontendInput, + $isSortable, $context, $expected ) { @@ -82,7 +84,7 @@ public function testGetFieldName( ->willReturn('string'); $attributeMock = $this->getMockBuilder(AttributeAdapter::class) ->disableOriginalConstructor() - ->setMethods(['getAttributeCode', 'getFrontendInput']) + ->setMethods(['getAttributeCode', 'getFrontendInput', 'isSortable']) ->getMock(); $attributeMock->expects($this->any()) ->method('getAttributeCode') @@ -90,6 +92,9 @@ public function testGetFieldName( $attributeMock->expects($this->any()) ->method('getFrontendInput') ->willReturn($frontendInput); + $attributeMock->expects($this->any()) + ->method('isSortable') + ->willReturn($isSortable); $this->fieldTypeResolver->expects($this->any()) ->method('getFieldType') ->willReturn($fieldType); @@ -106,13 +111,13 @@ public function testGetFieldName( public function getFieldNameProvider() { return [ - ['', 'code', '', [], 'code'], - ['', 'code', '', ['type' => 'default'], 'code'], - ['string', '*', '', ['type' => 'default'], '_all'], - ['', 'code', '', ['type' => 'default'], 'code'], - ['', 'code', 'select', ['type' => 'default'], 'code'], - ['', 'code', 'boolean', ['type' => 'default'], 'code'], - ['', 'code', '', ['type' => 'type'], 'sort_code'], + ['', 'code', '', false, [], 'code'], + ['', 'code', '', false, ['type' => 'default'], 'code'], + ['string', '*', '', false, ['type' => 'default'], '_all'], + ['', 'code', '', false, ['type' => 'default'], 'code'], + ['', 'code', 'select', false, ['type' => 'default'], 'code'], + ['', 'code', 'boolean', false, ['type' => 'default'], 'code'], + ['', 'code', '', true, ['type' => 'sort'], 'sort_code'], ]; } } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php index bf8b601ed43ab..de85b8b6602b8 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php @@ -20,6 +20,8 @@ as FieldTypeResolver; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ResolverInterface as FieldIndexResolver; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface + as FieldNameResolver; /** * @SuppressWarnings(PHPMD) @@ -61,6 +63,11 @@ class StaticFieldTest extends \PHPUnit\Framework\TestCase */ private $fieldTypeResolver; + /** + * @var FieldNameResolver + */ + private $fieldNameResolver; + /** * Set up test environment * @@ -90,6 +97,10 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['getFieldIndex']) ->getMock(); + $this->fieldNameResolver = $this->getMockBuilder(FieldNameResolver::class) + ->disableOriginalConstructor() + ->setMethods(['getFieldName']) + ->getMock(); $objectManager = new ObjectManagerHelper($this); @@ -102,6 +113,7 @@ protected function setUp() 'attributeAdapterProvider' => $this->attributeAdapterProvider, 'fieldIndexResolver' => $this->fieldIndexResolver, 'fieldTypeResolver' => $this->fieldTypeResolver, + 'fieldNameResolver' => $this->fieldNameResolver, ] ); } @@ -113,6 +125,10 @@ protected function setUp() * @param $indexType * @param $isComplexType * @param $complexType + * @param $isSortable + * @param $fieldName + * @param $compositeFieldName + * @param $sortFieldName * @param array $expected * @return void */ @@ -122,6 +138,10 @@ public function testGetAllAttributesTypes( $indexType, $isComplexType, $complexType, + $isSortable, + $fieldName, + $compositeFieldName, + $sortFieldName, $expected ) { $this->fieldTypeResolver->expects($this->any()) @@ -132,7 +152,30 @@ public function testGetAllAttributesTypes( ->willReturn($indexType); $this->indexTypeConverter->expects($this->any()) ->method('convert') - ->willReturn('no'); + ->with($this->anything()) + ->will($this->returnCallback( + function ($type) { + if ($type === 'no_index') { + return 'no'; + } elseif ($type === 'no_analyze') { + return 'not_analyzed'; + } + } + )); + $this->fieldNameResolver->expects($this->any()) + ->method('getFieldName') + ->with($this->anything()) + ->will($this->returnCallback( + function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortFieldName) { + if (empty($context)) { + return $fieldName; + } elseif ($context['type'] === 'sort') { + return $sortFieldName; + } elseif ($context['type'] === 'text') { + return $compositeFieldName; + } + } + )); $productAttributeMock = $this->getMockBuilder(AbstractAttribute::class) ->setMethods(['getAttributeCode']) @@ -146,11 +189,14 @@ public function testGetAllAttributesTypes( $attributeMock = $this->getMockBuilder(AttributeAdapter::class) ->disableOriginalConstructor() - ->setMethods(['isComplexType', 'getAttributeCode']) + ->setMethods(['isComplexType', 'getAttributeCode', 'isSortable']) ->getMock(); $attributeMock->expects($this->any()) ->method('isComplexType') ->willReturn($isComplexType); + $attributeMock->expects($this->any()) + ->method('isSortable') + ->willReturn($isSortable); $attributeMock->expects($this->any()) ->method('getAttributeCode') ->willReturn($attributeCode); @@ -166,13 +212,12 @@ function ($type) use ($complexType) { static $callCount = []; $callCount[$type] = !isset($callCount[$type]) ? 1 : ++$callCount[$type]; - if ($type === 'string') { - return 'string'; - } if ($type === 'string') { return 'string'; } elseif ($type === 'float') { return 'float'; + } elseif ($type === 'keyword') { + return 'string'; } else { return $complexType; } @@ -197,6 +242,10 @@ public function attributeProvider() true, true, 'text', + false, + 'category_ids', + 'category_ids_value', + '', [ 'category_ids' => [ 'type' => 'select', @@ -217,6 +266,10 @@ public function attributeProvider() 'no', false, null, + false, + 'attr_code', + '', + '', [ 'attr_code' => [ 'type' => 'text', @@ -234,6 +287,10 @@ public function attributeProvider() null, false, null, + false, + 'attr_code', + '', + '', [ 'attr_code' => [ 'type' => 'text' @@ -243,6 +300,32 @@ public function attributeProvider() 'index' => 'no' ] ] + ], + [ + 'attr_code', + 'text', + null, + false, + null, + true, + 'attr_code', + '', + 'sort_attr_code', + [ + 'attr_code' => [ + 'type' => 'text', + 'fields' => [ + 'sort_attr_code' => [ + 'type' => 'string', + 'index' => 'not_analyzed' + ] + ] + ], + 'store_id' => [ + 'type' => 'string', + 'index' => 'no' + ] + ] ] ]; } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/SortTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/SortTest.php new file mode 100644 index 0000000000000..efd9073694129 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/SortTest.php @@ -0,0 +1,237 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\Test\Unit\SearchAdapter\Query\Builder; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface + as FieldNameResolver; +use Magento\Framework\Search\RequestInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\Elasticsearch\SearchAdapter\Query\Builder\Sort; + +/** + * Class SortTest + */ +class SortTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var AttributeProvider + */ + private $attributeAdapterProvider; + + /** + * @var FieldNameResolver + */ + private $fieldNameResolver; + + /** + * @var Sort + */ + private $sortBuilder; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->attributeAdapterProvider = $this->getMockBuilder(AttributeProvider::class) + ->disableOriginalConstructor() + ->setMethods(['getByAttributeCode']) + ->getMock(); + $this->fieldNameResolver = $this->getMockBuilder(FieldNameResolver::class) + ->disableOriginalConstructor() + ->setMethods(['getFieldName']) + ->getMock(); + + $this->sortBuilder = (new ObjectManager($this))->getObject( + Sort::class, + [ + 'attributeAdapterProvider' => $this->attributeAdapterProvider, + 'fieldNameResolver' => $this->fieldNameResolver, + ] + ); + } + + /** + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @dataProvider getSortProvider + * @param array $sortItems + * @param $isSortable + * @param $isFloatType + * @param $isIntegerType + * @param $fieldName + * @param array $expected + */ + public function testGetSort( + array $sortItems, + $isSortable, + $isFloatType, + $isIntegerType, + $fieldName, + array $expected + ) { + /** @var MockObject|RequestInterface $request */ + $request = $this->getMockBuilder(RequestInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getSort']) + ->getMockForAbstractClass(); + $request->expects($this->any()) + ->method('getSort') + ->willReturn($sortItems); + $attributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods(['isSortable', 'isFloatType', 'isIntegerType']) + ->getMock(); + $attributeMock->expects($this->any()) + ->method('isSortable') + ->willReturn($isSortable); + $attributeMock->expects($this->any()) + ->method('isFloatType') + ->willReturn($isFloatType); + $attributeMock->expects($this->any()) + ->method('isIntegerType') + ->willReturn($isIntegerType); + $this->attributeAdapterProvider->expects($this->any()) + ->method('getByAttributeCode') + ->with($this->anything()) + ->willReturn($attributeMock); + $this->fieldNameResolver->expects($this->any()) + ->method('getFieldName') + ->with($this->anything()) + ->will($this->returnCallback( + function ($attribute, $context) use ($fieldName) { + if (empty($context)) { + return $fieldName; + } elseif ($context['type'] === 'sort') { + return 'sort_' . $fieldName; + } + } + )); + + $this->assertEquals( + $expected, + $this->sortBuilder->getSort($request) + ); + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array + */ + public function getSortProvider() + { + return [ + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ] + ], + null, + null, + null, + null, + [] + ], + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ], + [ + 'field' => 'price', + 'direction' => 'DESC' + ], + ], + false, + false, + false, + 'price', + [ + [ + 'price' => [ + 'order' => 'desc' + ] + ] + ] + ], + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ], + [ + 'field' => 'price', + 'direction' => 'DESC' + ], + ], + true, + true, + true, + 'price', + [ + [ + 'price' => [ + 'order' => 'desc' + ] + ] + ] + ], + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ], + [ + 'field' => 'name', + 'direction' => 'DESC' + ], + ], + true, + false, + false, + 'name', + [ + [ + 'name.sort_name' => [ + 'order' => 'desc' + ] + ] + ] + ], + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ], + [ + 'field' => 'not_eav_attribute', + 'direction' => 'DESC' + ], + ], + false, + false, + false, + 'not_eav_attribute', + [ + [ + 'not_eav_attribute' => [ + 'order' => 'desc' + ] + ] + ] + ] + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/ResponseFactoryTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/ResponseFactoryTest.php index 9ea241b2fbf5c..d89e420457206 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/ResponseFactoryTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/ResponseFactoryTest.php @@ -79,7 +79,7 @@ public function testCreate() 'itemTwo' => 45, ] ]; - $rawResponse = ['documents' => $documents, 'aggregations' => $aggregations]; + $rawResponse = ['documents' => $documents, 'aggregations' => $aggregations, 'total' => 2]; $exceptedResponse = [ 'documents' => [ @@ -102,6 +102,7 @@ public function testCreate() 'itemTwo' => 45 ], ], + 'total' => 2, ]; $this->documentFactory->expects($this->at(0))->method('create') @@ -118,7 +119,11 @@ public function testCreate() $this->objectManager->expects($this->once())->method('create') ->with( $this->equalTo(\Magento\Framework\Search\Response\QueryResponse::class), - $this->equalTo(['documents' => ['document1', 'document2'], 'aggregations' => 'aggregationsData']) + $this->equalTo([ + 'documents' => ['document1', 'document2'], + 'aggregations' => 'aggregationsData', + 'total' => 2 + ]) ) ->will($this->returnValue('QueryResponseObject')); diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml index ad549d5d37705..9796dc93858f1 100644 --- a/app/code/Magento/Elasticsearch/etc/di.xml +++ b/app/code/Magento/Elasticsearch/etc/di.xml @@ -13,6 +13,116 @@ <preference for="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ConverterInterface" type="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\Converter" /> <preference for="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface" type="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter" /> <preference for="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface" type="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\CompositeFieldProvider" /> + + <virtualType name="Magento\Elasticsearch\Model\Layer\Search\Context" type="Magento\Catalog\Model\Layer\Search\Context"> + <arguments> + <argument name="collectionProvider" xsi:type="object">elasticsearchLayerSearchItemCollectionProvider</argument> + <argument name="stateKey" xsi:type="object">Magento\CatalogSearch\Model\Layer\Search\StateKey</argument> + </arguments> + </virtualType> + <type name="Magento\Catalog\Model\Layer\Search"> + <arguments> + <argument name="context" xsi:type="object">Magento\Elasticsearch\Model\Layer\Search\Context</argument> + </arguments> + </type> + <virtualType name="Magento\Elasticsearch\Model\Layer\Category\Context" type="Magento\Catalog\Model\Layer\Category\Context"> + <arguments> + <argument name="collectionProvider" xsi:type="object">elasticsearchLayerCategoryItemCollectionProvider</argument> + </arguments> + </virtualType> + <type name="Magento\Catalog\Model\Layer\Category"> + <arguments> + <argument name="context" xsi:type="object">Magento\Elasticsearch\Model\Layer\Category\Context</argument> + </arguments> + </type> + <virtualType name="elasticsearchFulltextSearchCollection" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection"> + <arguments> + <argument name="searchRequestName" xsi:type="string">quick_search_container</argument> + <argument name="searchCriteriaResolverFactory" xsi:type="object">elasticsearchSearchCriteriaResolverFactory</argument> + <argument name="searchResultApplierFactory" xsi:type="object">elasticsearchSearchResultApplier\Factory</argument> + <argument name="totalRecordsResolverFactory" xsi:type="object">elasticsearchTotalRecordsResolver\Factory</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchFulltextSearchCollectionFactory" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\SearchCollectionFactory"> + <arguments> + <argument name="instanceName" xsi:type="string">elasticsearchFulltextSearchCollection</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchLayerSearchItemCollectionProvider" type="Magento\Elasticsearch\Model\Layer\Search\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="mysql" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Fulltext\SearchCollectionFactory</item> + <item name="elasticsearch" xsi:type="object">elasticsearchFulltextSearchCollectionFactory</item> + <item name="elasticsearch5" xsi:type="object">elasticsearchFulltextSearchCollectionFactory</item> + </argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchCategoryCollection" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection"> + <arguments> + <argument name="searchRequestName" xsi:type="string">catalog_view_container</argument> + <argument name="searchCriteriaResolverFactory" xsi:type="object">elasticsearchSearchCriteriaResolverFactory</argument> + <argument name="searchResultApplierFactory" xsi:type="object">elasticsearchSearchResultApplier\Factory</argument> + <argument name="totalRecordsResolverFactory" xsi:type="object">elasticsearchTotalRecordsResolver\Factory</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchCategoryCollectionFactory" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\SearchCollectionFactory"> + <arguments> + <argument name="instanceName" xsi:type="string">elasticsearchCategoryCollection</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchLayerCategoryItemCollectionProvider" type="Magento\Elasticsearch\Model\Layer\Category\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="mysql" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Fulltext\CollectionFactory</item> + <item name="elasticsearch" xsi:type="object">elasticsearchCategoryCollectionFactory</item> + <item name="elasticsearch5" xsi:type="object">elasticsearchCategoryCollectionFactory</item> + </argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchAdvancedCollection" type="Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection"> + <arguments> + <argument name="searchRequestName" xsi:type="string">advanced_search_container</argument> + <argument name="searchCriteriaResolverFactory" xsi:type="object">elasticsearchSearchCriteriaResolverFactory</argument> + <argument name="searchResultApplierFactory" xsi:type="object">elasticsearchSearchResultApplier\Factory</argument> + <argument name="totalRecordsResolverFactory" xsi:type="object">elasticsearchTotalRecordsResolver\Factory</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchAdvancedCollectionFactory" type="Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory"> + <arguments> + <argument name="instanceName" xsi:type="string">elasticsearchAdvancedCollection</argument> + </arguments> + </virtualType> + <type name="Magento\CatalogSearch\Model\Search\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="elasticsearch" xsi:type="object">elasticsearchAdvancedCollectionFactory</item> + <item name="elasticsearch5" xsi:type="object">elasticsearchAdvancedCollectionFactory</item> + </argument> + </arguments> + </type> + <type name="Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyProvider"> + <arguments> + <argument name="strategies" xsi:type="array"> + <item name="elasticsearch" xsi:type="object">Magento\Elasticsearch\Model\Advanced\ProductCollectionPrepareStrategy</item> + <item name="elasticsearch5" xsi:type="object">Magento\Elasticsearch\Model\Advanced\ProductCollectionPrepareStrategy</item> + </argument> + </arguments> + </type> + <virtualType name="elasticsearchSearchCriteriaResolverFactory" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory"> + <arguments> + <argument name="instanceName" xsi:type="string">Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolver</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchSearchResultApplier\Factory" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory"> + <arguments> + <argument name="instanceName" xsi:type="string">Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplier</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchTotalRecordsResolver\Factory" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory"> + <arguments> + <argument name="instanceName" xsi:type="string">Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolver</argument> + </arguments> + </virtualType> <type name="Magento\Elasticsearch\Model\Adapter\FieldMapper\FieldMapperResolver"> <arguments> <argument name="fieldMappers" xsi:type="array"> @@ -21,7 +131,7 @@ </arguments> </type> <preference for="Magento\Elasticsearch\Model\Adapter\DataMapperInterface" type="Magento\Elasticsearch\Model\Adapter\DataMapper\DataMapperResolver" /> - <virtualType name="AdditionalFieldsForElasticsearchDataMapper" type="Magento\AdvancedSearch\Model\Adapter\DataMapper\AdditionalFieldsProvider"> + <virtualType name="additionalFieldsProviderForElasticsearch" type="Magento\AdvancedSearch\Model\Adapter\DataMapper\AdditionalFieldsProvider"> <arguments> <argument name="fieldsProviders" xsi:type="array"> <item name="categories" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProviderProxy</item> @@ -31,7 +141,7 @@ </virtualType> <type name="Magento\Elasticsearch\Model\Adapter\BatchDataMapper\ProductDataMapper"> <arguments> - <argument name="additionalFieldsProvider" xsi:type="object">AdditionalFieldsForElasticsearchDataMapper</argument> + <argument name="additionalFieldsProvider" xsi:type="object">additionalFieldsProviderForElasticsearch</argument> </arguments> </type> <preference for="Magento\Elasticsearch\Model\Adapter\BatchDataMapperInterface" type="Magento\Elasticsearch\Model\Adapter\BatchDataMapper\DataMapperResolver" /> @@ -360,6 +470,7 @@ <argument name="indexTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\Converter</argument> <argument name="fieldIndexResolver" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\IndexResolver</argument> <argument name="fieldTypeResolver" xsi:type="object">\Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver</argument> + <argument name="fieldNameResolver" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface</argument> </arguments> </virtualType> <virtualType name="elasticsearch5DynamicFieldProvider" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\DynamicField"> diff --git a/app/code/Magento/Elasticsearch6/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php index c2a70d3f082f0..a3c6e7e148f3d 100644 --- a/app/code/Magento/Elasticsearch6/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php +++ b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php @@ -66,6 +66,7 @@ protected function setUp() * @param $fieldType * @param $attributeCode * @param $frontendInput + * @param $isSortable * @param $context * @param $expected * @return void @@ -74,6 +75,7 @@ public function testGetFieldName( $fieldType, $attributeCode, $frontendInput, + $isSortable, $context, $expected ) { @@ -82,7 +84,7 @@ public function testGetFieldName( ->willReturn('string'); $attributeMock = $this->getMockBuilder(AttributeAdapter::class) ->disableOriginalConstructor() - ->setMethods(['getAttributeCode', 'getFrontendInput']) + ->setMethods(['getAttributeCode', 'getFrontendInput', 'isSortable']) ->getMock(); $attributeMock->expects($this->any()) ->method('getAttributeCode') @@ -90,6 +92,9 @@ public function testGetFieldName( $attributeMock->expects($this->any()) ->method('getFrontendInput') ->willReturn($frontendInput); + $attributeMock->expects($this->any()) + ->method('isSortable') + ->willReturn($isSortable); $this->fieldTypeResolver->expects($this->any()) ->method('getFieldType') ->willReturn($fieldType); @@ -106,13 +111,13 @@ public function testGetFieldName( public function getFieldNameProvider() { return [ - ['', 'code', '', [], 'code'], - ['', 'code', '', ['type' => 'default'], 'code'], - ['string', '*', '', ['type' => 'default'], '_search'], - ['', 'code', '', ['type' => 'default'], 'code'], - ['', 'code', 'select', ['type' => 'default'], 'code'], - ['', 'code', 'boolean', ['type' => 'default'], 'code'], - ['', 'code', '', ['type' => 'type'], 'sort_code'], + ['', 'code', '', false, [], 'code'], + ['', 'code', '', false, ['type' => 'default'], 'code'], + ['string', '*', '', false, ['type' => 'default'], '_search'], + ['', 'code', '', false, ['type' => 'default'], 'code'], + ['', 'code', 'select', false, ['type' => 'default'], 'code'], + ['', 'code', 'boolean', false, ['type' => 'default'], 'code'], + ['', 'code', '', true, ['type' => 'sort'], 'sort_code'], ]; } } diff --git a/app/code/Magento/GoogleAnalytics/Block/Ga.php b/app/code/Magento/GoogleAnalytics/Block/Ga.php index 7d065ea50b369..b5917407b60ae 100644 --- a/app/code/Magento/GoogleAnalytics/Block/Ga.php +++ b/app/code/Magento/GoogleAnalytics/Block/Ga.php @@ -75,7 +75,8 @@ public function getPageName() } /** - * Render regular page tracking javascript code + * Render regular page tracking javascript code. + * * The custom "page name" may be set from layout or somewhere else. It must start from slash. * * @param string $accountId diff --git a/app/code/Magento/GoogleAnalytics/Helper/Data.php b/app/code/Magento/GoogleAnalytics/Helper/Data.php index 2af03c71fb1b0..90a207921d51f 100644 --- a/app/code/Magento/GoogleAnalytics/Helper/Data.php +++ b/app/code/Magento/GoogleAnalytics/Helper/Data.php @@ -46,6 +46,6 @@ public function isGoogleAnalyticsAvailable($store = null) */ public function isAnonymizedIpActive($store = null) { - return $this->scopeConfig->getValue(self::XML_PATH_ANONYMIZE, ScopeInterface::SCOPE_STORE, $store); + return (bool)$this->scopeConfig->getValue(self::XML_PATH_ANONYMIZE, ScopeInterface::SCOPE_STORE, $store); } } diff --git a/app/code/Magento/MediaStorage/Service/ImageResize.php b/app/code/Magento/MediaStorage/Service/ImageResize.php index 6e3929296e252..aae90512b3d95 100644 --- a/app/code/Magento/MediaStorage/Service/ImageResize.php +++ b/app/code/Magento/MediaStorage/Service/ImageResize.php @@ -24,6 +24,8 @@ use Magento\Framework\App\Filesystem\DirectoryList; /** + * Image resize service. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ImageResize @@ -123,7 +125,8 @@ public function __construct( } /** - * Create resized images of different sizes from an original image + * Create resized images of different sizes from an original image. + * * @param string $originalImageName * @throws NotFoundException */ @@ -141,7 +144,8 @@ public function resizeFromImageName(string $originalImageName) } /** - * Create resized images of different sizes from themes + * Create resized images of different sizes from themes. + * * @param array|null $themes * @return \Generator * @throws NotFoundException @@ -169,7 +173,8 @@ public function resizeFromThemes(array $themes = null): \Generator } /** - * Search the current theme + * Search the current theme. + * * @return array */ private function getThemesInUse(): array @@ -187,7 +192,8 @@ private function getThemesInUse(): array } /** - * Get view images data from themes + * Get view images data from themes. + * * @param array $themes * @return array */ @@ -211,7 +217,8 @@ private function getViewImages(array $themes): array } /** - * Get unique image index + * Get unique image index. + * * @param array $imageData * @return string */ @@ -223,7 +230,8 @@ private function getUniqueImageIndex(array $imageData): string } /** - * Make image + * Make image. + * * @param string $originalImagePath * @param array $imageParams * @return Image @@ -241,7 +249,8 @@ private function makeImage(string $originalImagePath, array $imageParams): Image } /** - * Resize image + * Resize image. + * * @param array $viewImage * @param string $originalImagePath * @param string $originalImageName @@ -257,9 +266,41 @@ private function resize(array $viewImage, string $originalImagePath, string $ori ] ); + if (isset($imageParams['watermark_file'])) { + if ($imageParams['watermark_height'] !== null) { + $image->setWatermarkHeight($imageParams['watermark_height']); + } + + if ($imageParams['watermark_width'] !== null) { + $image->setWatermarkWidth($imageParams['watermark_width']); + } + + if ($imageParams['watermark_position'] !== null) { + $image->setWatermarkPosition($imageParams['watermark_position']); + } + + if ($imageParams['watermark_image_opacity'] !== null) { + $image->setWatermarkImageOpacity($imageParams['watermark_image_opacity']); + } + + $image->watermark($this->getWatermarkFilePath($imageParams['watermark_file'])); + } + if ($imageParams['image_width'] !== null && $imageParams['image_height'] !== null) { $image->resize($imageParams['image_width'], $imageParams['image_height']); } $image->save($imageAsset->getPath()); } + + /** + * Returns watermark file absolute path + * + * @param string $file + * @return string + */ + private function getWatermarkFilePath($file) + { + $path = $this->imageConfig->getMediaPath('/watermark/' . $file); + return $this->mediaDirectory->getAbsolutePath($path); + } } diff --git a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/ShippingOptionsCallback.php b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/ShippingOptionsCallback.php index cb1b3388dc06a..fc3a45e1e1397 100644 --- a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/ShippingOptionsCallback.php +++ b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/ShippingOptionsCallback.php @@ -6,7 +6,17 @@ */ namespace Magento\Paypal\Controller\Express\AbstractExpress; -class ShippingOptionsCallback extends \Magento\Paypal\Controller\Express\AbstractExpress +use Magento\Framework\App\CsrfAwareActionInterface; +use Magento\Paypal\Controller\Express\AbstractExpress; +use Magento\Framework\App\Request\InvalidRequestException; +use Magento\Framework\App\RequestInterface; + +/** + * Returns shipping rates by server-to-server request from PayPal. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ShippingOptionsCallback extends AbstractExpress implements CsrfAwareActionInterface { /** * @var \Magento\Quote\Api\CartRepositoryInterface @@ -65,4 +75,21 @@ public function execute() $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); } } + + /** + * @inheritDoc + */ + public function createCsrfValidationException( + RequestInterface $request + ): ?InvalidRequestException { + return null; + } + + /** + * @inheritDoc + */ + public function validateForCsrf(RequestInterface $request): ?bool + { + return true; + } } diff --git a/app/code/Magento/Paypal/Model/Express/Checkout.php b/app/code/Magento/Paypal/Model/Express/Checkout.php index 38ba0983514b0..72f166e8d07c1 100644 --- a/app/code/Magento/Paypal/Model/Express/Checkout.php +++ b/app/code/Magento/Paypal/Model/Express/Checkout.php @@ -1076,6 +1076,7 @@ protected static function cmpShippingOptions(DataObject $option1, DataObject $op */ protected function _matchShippingMethodCode(Address $address, $selectedCode) { + $address->collectShippingRates(); $options = $this->_prepareShippingOptions($address, false); foreach ($options as $option) { if ($selectedCode === $option['code'] // the proper case as outlined in documentation diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AssignBillingAddressToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignBillingAddressToCart.php index 370501e9c6e8e..dd6478b4873c6 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AssignBillingAddressToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignBillingAddressToCart.php @@ -11,9 +11,9 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Api\Data\CartInterface; use Magento\Quote\Api\BillingAddressManagementInterface; -use Magento\Quote\Model\Quote\Address as QuoteAddress; /** * Set billing address for a specified shopping cart @@ -38,22 +38,22 @@ public function __construct( * Assign billing address to cart * * @param CartInterface $cart - * @param QuoteAddress $billingAddress + * @param AddressInterface $billingAddress * @param bool $useForShipping * @throws GraphQlInputException * @throws GraphQlNoSuchEntityException */ public function execute( CartInterface $cart, - QuoteAddress $billingAddress, + AddressInterface $billingAddress, bool $useForShipping ): void { try { $this->billingAddressManagement->assign($cart->getId(), $billingAddress, $useForShipping); } catch (NoSuchEntityException $e) { - throw new GraphQlNoSuchEntityException(__($e->getMessage())); + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); } catch (LocalizedException $e) { - throw new GraphQlInputException(__($e->getMessage())); + throw new GraphQlInputException(__($e->getMessage()), $e); } } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingAddressToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingAddressToCart.php index 47f90edb04be8..527999b245a4c 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingAddressToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingAddressToCart.php @@ -11,8 +11,8 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Api\Data\CartInterface; -use Magento\Quote\Model\Quote\Address as QuoteAddress; use Magento\Quote\Model\ShippingAddressManagementInterface; /** @@ -38,20 +38,20 @@ public function __construct( * Assign shipping address to cart * * @param CartInterface $cart - * @param QuoteAddress $shippingAddress + * @param AddressInterface $shippingAddress * @throws GraphQlInputException * @throws GraphQlNoSuchEntityException */ public function execute( CartInterface $cart, - QuoteAddress $shippingAddress + AddressInterface $shippingAddress ): void { try { $this->shippingAddressManagement->assign($cart->getId(), $shippingAddress); } catch (NoSuchEntityException $e) { - throw new GraphQlNoSuchEntityException(__($e->getMessage())); + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); } catch (LocalizedException $e) { - throw new GraphQlInputException(__($e->getMessage())); + throw new GraphQlInputException(__($e->getMessage()), $e); } } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingMethodToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingMethodToCart.php new file mode 100644 index 0000000000000..5b30c0774c22f --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingMethodToCart.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Checkout\Api\Data\ShippingInformationInterface; +use Magento\Checkout\Api\Data\ShippingInformationInterfaceFactory; +use Magento\Checkout\Api\ShippingInformationManagementInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\CartInterface; + +/** + * Assign shipping method to cart + */ +class AssignShippingMethodToCart +{ + /** + * @var ShippingInformationInterfaceFactory + */ + private $shippingInformationFactory; + + /** + * @var ShippingInformationManagementInterface + */ + private $shippingInformationManagement; + + /** + * @param ShippingInformationInterfaceFactory $shippingInformationFactory + * @param ShippingInformationManagementInterface $shippingInformationManagement + */ + public function __construct( + ShippingInformationInterfaceFactory $shippingInformationFactory, + ShippingInformationManagementInterface $shippingInformationManagement + ) { + $this->shippingInformationFactory = $shippingInformationFactory; + $this->shippingInformationManagement = $shippingInformationManagement; + } + + /** + * Assign shipping method to cart + * + * @param CartInterface $cart + * @param AddressInterface $quoteAddress + * @param string $carrierCode + * @param string $methodCode + * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException + */ + public function execute( + CartInterface $cart, + AddressInterface $quoteAddress, + string $carrierCode, + string $methodCode + ): void { + /** @var ShippingInformationInterface $shippingInformation */ + $shippingInformation = $this->shippingInformationFactory->create([ + 'data' => [ + /* If the address is not a shipping address (but billing) the system will find the proper shipping + address for the selected cart and set the information there (actual for single shipping address) */ + ShippingInformationInterface::SHIPPING_ADDRESS => $quoteAddress, + ShippingInformationInterface::SHIPPING_CARRIER_CODE => $carrierCode, + ShippingInformationInterface::SHIPPING_METHOD_CODE => $methodCode, + ], + ]); + + try { + $this->shippingInformationManagement->saveAddressInformation($cart->getId(), $shippingInformation); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractDataFromAddress.php b/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractDataFromAddress.php index a52745a8bb744..20212d3412595 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractDataFromAddress.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractDataFromAddress.php @@ -7,6 +7,7 @@ namespace Magento\QuoteGraphQl\Model\Cart; +use Magento\Customer\Model\Address\AbstractAddress; use Magento\Framework\Api\ExtensibleDataObjectConverter; use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Model\Quote\Address as QuoteAddress; @@ -40,8 +41,17 @@ public function execute(QuoteAddress $address): array $addressData = $this->dataObjectConverter->toFlatArray($address, [], AddressInterface::class); $addressData['model'] = $address; + if ($address->getAddressType() == AbstractAddress::TYPE_SHIPPING) { + $addressType = 'SHIPPING'; + } elseif ($address->getAddressType() == AbstractAddress::TYPE_BILLING) { + $addressType = 'BILLING'; + } else { + $addressType = null; + } + $addressData = array_merge($addressData, [ 'address_id' => $address->getId(), + 'address_type' => $addressType, 'country' => [ 'code' => $address->getCountryId(), 'label' => $address->getCountry() @@ -51,12 +61,6 @@ public function execute(QuoteAddress $address): array 'label' => $address->getRegion() ], 'street' => $address->getStreet(), - 'selected_shipping_method' => [ - 'code' => $address->getShippingMethod(), - 'label' => $address->getShippingDescription(), - 'free_shipping' => $address->getFreeShipping(), - 'amount' => $address->getShippingAmount() - ], 'items_weight' => $address->getWeight(), 'customer_notes' => $address->getCustomerNotes() ]); diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/GetCustomerAddress.php b/app/code/Magento/QuoteGraphQl/Model/Cart/GetCustomerAddress.php index 93c888a1d0bd2..039324abf6854 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/GetCustomerAddress.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/GetCustomerAddress.php @@ -16,7 +16,7 @@ use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; /** - * Get customer address. Throws exception if customer is not owner of address + * Get customer address */ class GetCustomerAddress { @@ -52,7 +52,7 @@ public function execute(int $addressId, int $customerId): AddressInterface __('Could not find a address with ID "%address_id"', ['address_id' => $addressId]) ); } catch (LocalizedException $e) { - throw new GraphQlInputException(__($e->getMessage())); + throw new GraphQlInputException(__($e->getMessage()), $e); } if ((int)$customerAddress->getCustomerId() !== $customerId) { diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/GetQuoteAddress.php b/app/code/Magento/QuoteGraphQl/Model/Cart/GetQuoteAddress.php new file mode 100644 index 0000000000000..1fb737d964139 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/GetQuoteAddress.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Model\ResourceModel\Quote\Address as AddressResource; + +/** + * Get quote address + */ +class GetQuoteAddress +{ + /** + * @var AddressInterfaceFactory + */ + private $quoteAddressFactory; + + /** + * @var AddressResource + */ + private $quoteAddressResource; + + /** + * @param AddressInterfaceFactory $quoteAddressFactory + * @param AddressResource $quoteAddressResource + */ + public function __construct( + AddressInterfaceFactory $quoteAddressFactory, + AddressResource $quoteAddressResource + ) { + $this->quoteAddressFactory = $quoteAddressFactory; + $this->quoteAddressResource = $quoteAddressResource; + } + + /** + * Get quote address + * + * @param int $quoteAddressId + * @param int|null $customerId + * @return AddressInterface + * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException + * @throws GraphQlAuthorizationException + */ + public function execute(int $quoteAddressId, ?int $customerId): AddressInterface + { + $quoteAddress = $this->quoteAddressFactory->create(); + + $this->quoteAddressResource->load($quoteAddress, $quoteAddressId); + if (null === $quoteAddress->getId()) { + throw new GraphQlNoSuchEntityException( + __('Could not find a cart address with ID "%cart_address_id"', ['cart_address_id' => $quoteAddressId]) + ); + } + + $quoteAddressCustomerId = (int)$quoteAddress->getCustomerId(); + + /* Guest cart, allow operations */ + if (!$quoteAddressCustomerId && null === $customerId) { + return $quoteAddress; + } + + if ($quoteAddressCustomerId !== $customerId) { + throw new GraphQlAuthorizationException( + __( + 'The current user cannot use cart address with ID "%cart_address_id"', + ['cart_address_id' => $quoteAddressId] + ) + ); + } + return $quoteAddress; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/QuoteAddressFactory.php b/app/code/Magento/QuoteGraphQl/Model/Cart/QuoteAddressFactory.php index 7dfea0836e8d6..76bdc74611131 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/QuoteAddressFactory.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/QuoteAddressFactory.php @@ -60,7 +60,7 @@ public function createBasedOnCustomerAddress(CustomerAddress $customerAddress): try { $quoteAddress->importCustomerAddressData($customerAddress); } catch (LocalizedException $e) { - throw new GraphQlInputException(__($e->getMessage())); + throw new GraphQlInputException(__($e->getMessage()), $e); } return $quoteAddress; } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php index 04b7bfcfe0e62..cf277c729cdfd 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php @@ -63,19 +63,19 @@ public function __construct( * * @param ContextInterface $context * @param CartInterface $cart - * @param array $billingAddress + * @param array $billingAddressInput * @return void * @throws GraphQlInputException * @throws GraphQlAuthenticationException * @throws GraphQlAuthorizationException * @throws GraphQlNoSuchEntityException */ - public function execute(ContextInterface $context, CartInterface $cart, array $billingAddress): void + public function execute(ContextInterface $context, CartInterface $cart, array $billingAddressInput): void { - $customerAddressId = $billingAddress['customer_address_id'] ?? null; - $addressInput = $billingAddress['address'] ?? null; - $useForShipping = isset($billingAddress['use_for_shipping']) - ? (bool)$billingAddress['use_for_shipping'] : false; + $customerAddressId = $billingAddressInput['customer_address_id'] ?? null; + $addressInput = $billingAddressInput['address'] ?? null; + $useForShipping = isset($billingAddressInput['use_for_shipping']) + ? (bool)$billingAddressInput['use_for_shipping'] : false; if (null === $customerAddressId && null === $addressInput) { throw new GraphQlInputException( diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php similarity index 88% rename from app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressOnCart.php rename to app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php index 1a14424853491..8e54ab0d3feef 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php @@ -15,7 +15,7 @@ /** * Set single shipping address for a specified shopping cart */ -class SetShippingAddressOnCart implements SetShippingAddressesOnCartInterface +class SetShippingAddressesOnCart implements SetShippingAddressesOnCartInterface { /** * @var QuoteAddressFactory @@ -58,16 +58,16 @@ public function __construct( /** * @inheritdoc */ - public function execute(ContextInterface $context, CartInterface $cart, array $shippingAddresses): void + public function execute(ContextInterface $context, CartInterface $cart, array $shippingAddressesInput): void { - if (count($shippingAddresses) > 1) { + if (count($shippingAddressesInput) > 1) { throw new GraphQlInputException( __('You cannot specify multiple shipping addresses.') ); } - $shippingAddress = current($shippingAddresses); - $customerAddressId = $shippingAddress['customer_address_id'] ?? null; - $addressInput = $shippingAddress['address'] ?? null; + $shippingAddressInput = current($shippingAddressesInput); + $customerAddressId = $shippingAddressInput['customer_address_id'] ?? null; + $addressInput = $shippingAddressInput['address'] ?? null; if (null === $customerAddressId && null === $addressInput) { throw new GraphQlInputException( diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCartInterface.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCartInterface.php index 81da47933e812..eb0f3522102cf 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCartInterface.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCartInterface.php @@ -27,12 +27,12 @@ interface SetShippingAddressesOnCartInterface * * @param ContextInterface $context * @param CartInterface $cart - * @param array $shippingAddresses + * @param array $shippingAddressesInput * @return void * @throws GraphQlInputException * @throws GraphQlAuthorizationException * @throws GraphQlAuthenticationException * @throws GraphQlNoSuchEntityException */ - public function execute(ContextInterface $context, CartInterface $cart, array $shippingAddresses): void; + public function execute(ContextInterface $context, CartInterface $cart, array $shippingAddressesInput): void; } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodOnCart.php deleted file mode 100644 index a630b2d07c7df..0000000000000 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodOnCart.php +++ /dev/null @@ -1,101 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\QuoteGraphQl\Model\Cart; - -use Magento\Framework\Exception\InputException; -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Framework\Exception\StateException; -use Magento\Framework\GraphQl\Exception\GraphQlInputException; -use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; -use Magento\Quote\Model\Quote; -use Magento\Quote\Model\Quote\AddressFactory as QuoteAddressFactory; -use Magento\Quote\Model\ResourceModel\Quote\Address as QuoteAddressResource; -use Magento\Checkout\Model\ShippingInformationFactory; -use Magento\Checkout\Api\ShippingInformationManagementInterface; -use Magento\Checkout\Model\ShippingInformation; - -/** - * Class SetShippingMethodsOnCart - * - * Set shipping method for a specified shopping cart address - */ -class SetShippingMethodOnCart -{ - /** - * @var ShippingInformationFactory - */ - private $shippingInformationFactory; - - /** - * @var QuoteAddressFactory - */ - private $quoteAddressFactory; - - /** - * @var QuoteAddressResource - */ - private $quoteAddressResource; - - /** - * @var ShippingInformationManagementInterface - */ - private $shippingInformationManagement; - - /** - * @param ShippingInformationManagementInterface $shippingInformationManagement - * @param QuoteAddressFactory $quoteAddressFactory - * @param QuoteAddressResource $quoteAddressResource - * @param ShippingInformationFactory $shippingInformationFactory - */ - public function __construct( - ShippingInformationManagementInterface $shippingInformationManagement, - QuoteAddressFactory $quoteAddressFactory, - QuoteAddressResource $quoteAddressResource, - ShippingInformationFactory $shippingInformationFactory - ) { - $this->shippingInformationManagement = $shippingInformationManagement; - $this->quoteAddressResource = $quoteAddressResource; - $this->quoteAddressFactory = $quoteAddressFactory; - $this->shippingInformationFactory = $shippingInformationFactory; - } - - /** - * Sets shipping method for a specified shopping cart address - * - * @param Quote $cart - * @param int $cartAddressId - * @param string $carrierCode - * @param string $methodCode - * @throws GraphQlInputException - * @throws GraphQlNoSuchEntityException - */ - public function execute(Quote $cart, int $cartAddressId, string $carrierCode, string $methodCode): void - { - $quoteAddress = $this->quoteAddressFactory->create(); - $this->quoteAddressResource->load($quoteAddress, $cartAddressId); - - /** @var ShippingInformation $shippingInformation */ - $shippingInformation = $this->shippingInformationFactory->create(); - - /* If the address is not a shipping address (but billing) the system will find the proper shipping address for - the selected cart and set the information there (actual for single shipping address) */ - $shippingInformation->setShippingAddress($quoteAddress); - $shippingInformation->setShippingCarrierCode($carrierCode); - $shippingInformation->setShippingMethodCode($methodCode); - - try { - $this->shippingInformationManagement->saveAddressInformation($cart->getId(), $shippingInformation); - } catch (NoSuchEntityException $exception) { - throw new GraphQlNoSuchEntityException(__($exception->getMessage())); - } catch (StateException $exception) { - throw new GraphQlInputException(__($exception->getMessage())); - } catch (InputException $exception) { - throw new GraphQlInputException(__($exception->getMessage())); - } - } -} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php new file mode 100644 index 0000000000000..37e0118423745 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\StateException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Quote\Api\Data\CartInterface; + +/** + * Set single shipping method for a specified shopping cart + */ +class SetShippingMethodsOnCart implements SetShippingMethodsOnCartInterface +{ + /** + * @var GetQuoteAddress + */ + private $getQuoteAddress; + + /** + * @var AssignShippingMethodToCart + */ + private $assignShippingMethodToCart; + + /** + * @param GetQuoteAddress $getQuoteAddress + * @param AssignShippingMethodToCart $assignShippingMethodToCart + */ + public function __construct( + GetQuoteAddress $getQuoteAddress, + AssignShippingMethodToCart $assignShippingMethodToCart + ) { + $this->getQuoteAddress = $getQuoteAddress; + $this->assignShippingMethodToCart = $assignShippingMethodToCart; + } + + /** + * @inheritdoc + */ + public function execute(ContextInterface $context, CartInterface $cart, array $shippingMethodsInput): void + { + if (count($shippingMethodsInput) > 1) { + throw new GraphQlInputException( + __('You cannot specify multiple shipping methods.') + ); + } + $shippingMethodInput = current($shippingMethodsInput); + + if (!isset($shippingMethodInput['cart_address_id']) || empty($shippingMethodInput['cart_address_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_address_id" is missing.')); + } + $cartAddressId = $shippingMethodInput['cart_address_id']; + + if (!isset($shippingMethodInput['carrier_code']) || empty($shippingMethodInput['carrier_code'])) { + throw new GraphQlInputException(__('Required parameter "carrier_code" is missing.')); + } + $carrierCode = $shippingMethodInput['carrier_code']; + + if (!isset($shippingMethodInput['method_code']) || empty($shippingMethodInput['method_code'])) { + throw new GraphQlInputException(__('Required parameter "method_code" is missing.')); + } + $methodCode = $shippingMethodInput['method_code']; + + $quoteAddress = $this->getQuoteAddress->execute($cartAddressId, $context->getUserId()); + + $this->assignShippingMethodToCart->execute($cart, $quoteAddress, $carrierCode, $methodCode); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCartInterface.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCartInterface.php new file mode 100644 index 0000000000000..fa6c6cf0923e4 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCartInterface.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Quote\Api\Data\CartInterface; + +/** + * Extension point for setting shipping methods for a specified shopping cart + * + * All objects that are responsible for setting shipping methods on a cart via GraphQl + * should implement this interface. + */ +interface SetShippingMethodsOnCartInterface +{ + /** + * Set shipping methods for a specified shopping cart + * + * @param ContextInterface $context + * @param CartInterface $cart + * @param array $shippingMethodsInput + * @return void + * @throws GraphQlInputException + * @throws GraphQlAuthorizationException + * @throws GraphQlAuthenticationException + * @throws GraphQlNoSuchEntityException + */ + public function execute(ContextInterface $context, CartInterface $cart, array $shippingMethodsInput): void; +} diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Text.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Text.php index 4b29eb6a4a663..96f11badac82e 100644 --- a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Text.php +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Text.php @@ -42,6 +42,7 @@ public function getData( ): array { /** @var TextOptionType $optionTypeRenderer */ $optionTypeRenderer = $option->groupFactory($option->getType()); + $optionTypeRenderer->setOption($option); $priceValueUnits = $this->priceUnitLabel->getData($option->getPriceType()); $selectedOptionValueData = [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ApplyCouponToCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ApplyCouponToCart.php index a334e7482ca47..0af35354758c9 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/ApplyCouponToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ApplyCouponToCart.php @@ -74,10 +74,10 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value try { $this->couponManagement->set($cartId, $couponCode); - } catch (NoSuchEntityException $exception) { - throw new GraphQlNoSuchEntityException(__($exception->getMessage())); - } catch (CouldNotSaveException $exception) { - throw new LocalizedException(__($exception->getMessage())); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } catch (CouldNotSaveException $e) { + throw new LocalizedException(__($e->getMessage()), $e); } return [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveCouponFromCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveCouponFromCart.php index 730c927c32a0c..68925c6e8944d 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveCouponFromCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveCouponFromCart.php @@ -61,10 +61,10 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value try { $this->couponManagement->remove($cartId); - } catch (NoSuchEntityException $exception) { - throw new GraphQlNoSuchEntityException(__($exception->getMessage())); - } catch (CouldNotDeleteException $exception) { - throw new LocalizedException(__($exception->getMessage())); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } catch (CouldNotDeleteException $e) { + throw new LocalizedException(__($e->getMessage()), $e); } return [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php index 1838f17369ffc..c8f0ef06c7594 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php @@ -64,9 +64,9 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value try { $this->guestCartItemRepository->deleteById($maskedCartId, $itemId); } catch (NoSuchEntityException $e) { - throw new GraphQlNoSuchEntityException(__($e->getMessage())); + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); } catch (LocalizedException $e) { - throw new GraphQlInputException(__($e->getMessage())); + throw new GraphQlInputException(__($e->getMessage()), $e); } return [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php index ffd1bf37f4771..a93c8032c996a 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php @@ -86,9 +86,9 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value try { $this->paymentMethodManagement->set($cart->getId(), $payment); } catch (NoSuchEntityException $e) { - throw new GraphQlNoSuchEntityException(__($e->getMessage())); + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); } catch (LocalizedException $e) { - throw new GraphQlInputException(__($e->getMessage())); + throw new GraphQlInputException(__($e->getMessage()), $e); } return [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingMethodsOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingMethodsOnCart.php index f2e5be797dbd6..e69ba47e7adf5 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingMethodsOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingMethodsOnCart.php @@ -11,9 +11,8 @@ use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\Framework\Stdlib\ArrayManager; use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; -use Magento\QuoteGraphQl\Model\Cart\SetShippingMethodOnCart; +use Magento\QuoteGraphQl\Model\Cart\SetShippingMethodsOnCartInterface; /** * Mutation resolver for setting shipping methods for shopping cart @@ -21,33 +20,25 @@ class SetShippingMethodsOnCart implements ResolverInterface { /** - * @var SetShippingMethodOnCart - */ - private $setShippingMethodOnCart; - - /** - * @var ArrayManager + * @var GetCartForUser */ - private $arrayManager; + private $getCartForUser; /** - * @var GetCartForUser + * @var SetShippingMethodsOnCartInterface */ - private $getCartForUser; + private $setShippingMethodsOnCart; /** - * @param ArrayManager $arrayManager * @param GetCartForUser $getCartForUser - * @param SetShippingMethodOnCart $setShippingMethodOnCart + * @param SetShippingMethodsOnCartInterface $setShippingMethodsOnCart */ public function __construct( - ArrayManager $arrayManager, GetCartForUser $getCartForUser, - SetShippingMethodOnCart $setShippingMethodOnCart + SetShippingMethodsOnCartInterface $setShippingMethodsOnCart ) { - $this->arrayManager = $arrayManager; $this->getCartForUser = $getCartForUser; - $this->setShippingMethodOnCart = $setShippingMethodOnCart; + $this->setShippingMethodsOnCart = $setShippingMethodsOnCart; } /** @@ -55,37 +46,18 @@ public function __construct( */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { - $shippingMethods = $this->arrayManager->get('input/shipping_methods', $args); - $maskedCartId = $this->arrayManager->get('input/cart_id', $args); - - if (!$maskedCartId) { + if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); } - if (!$shippingMethods) { - throw new GraphQlInputException(__('Required parameter "shipping_methods" is missing')); - } + $maskedCartId = $args['input']['cart_id']; - $shippingMethod = reset($shippingMethods); // This point can be extended for multishipping - - if (!isset($shippingMethod['cart_address_id']) || empty($shippingMethod['cart_address_id'])) { - throw new GraphQlInputException(__('Required parameter "cart_address_id" is missing')); - } - if (!isset($shippingMethod['carrier_code']) || empty($shippingMethod['carrier_code'])) { - throw new GraphQlInputException(__('Required parameter "shipping_carrier_code" is missing')); - } - if (!isset($shippingMethod['method_code']) || empty($shippingMethod['method_code'])) { - throw new GraphQlInputException(__('Required parameter "shipping_method_code" is missing')); + if (!isset($args['input']['shipping_methods']) || empty($args['input']['shipping_methods'])) { + throw new GraphQlInputException(__('Required parameter "shipping_methods" is missing')); } + $shippingMethods = $args['input']['shipping_methods']; - $userId = $context->getUserId(); - $cart = $this->getCartForUser->execute((string) $maskedCartId, $userId); - - $this->setShippingMethodOnCart->execute( - $cart, - $shippingMethod['cart_address_id'], - $shippingMethod['carrier_code'], - $shippingMethod['method_code'] - ); + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId()); + $this->setShippingMethodsOnCart->execute($context, $cart, $shippingMethods); return [ 'cart' => [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAdress/AvailableShippingMethods.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/AvailableShippingMethods.php similarity index 97% rename from app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAdress/AvailableShippingMethods.php rename to app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/AvailableShippingMethods.php index 7804b8defe378..a9e0ba59d15d9 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAdress/AvailableShippingMethods.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/AvailableShippingMethods.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\QuoteGraphQl\Model\Resolver\ShippingAdress; +namespace Magento\QuoteGraphQl\Model\Resolver\ShippingAddress; use Magento\Framework\Api\ExtensibleDataObjectConverter; use Magento\Framework\Exception\LocalizedException; diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php new file mode 100644 index 0000000000000..c58affa064c89 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver\ShippingAddress; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * @inheritdoc + */ +class SelectedShippingMethod implements ResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + $address = $value['model']; + + if ($address->getShippingMethod()) { + list($carrierCode, $methodCode) = explode('_', $address->getShippingMethod(), 2); + $shippingAmount = $address->getShippingAmount(); + } + + return [ + 'carrier_code' => $carrierCode ?? null, + 'method_code' => $methodCode ?? null, + 'label' => $address->getShippingDescription(), + 'amount' => $shippingAmount ?? null, + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml b/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml index 86bc954ae4ac4..c7389cf667845 100644 --- a/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml @@ -7,5 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\QuoteGraphQl\Model\Cart\SetShippingAddressesOnCartInterface" - type="Magento\QuoteGraphQl\Model\Cart\SetShippingAddressOnCart" /> + type="Magento\QuoteGraphQl\Model\Cart\SetShippingAddressesOnCart"/> + <preference for="Magento\QuoteGraphQl\Model\Cart\SetShippingMethodsOnCartInterface" + type="Magento\QuoteGraphQl\Model\Cart\SetShippingMethodsOnCart"/> </config> diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index e4ced2351572c..5f65a990bac21 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -66,12 +66,6 @@ input SetShippingAddressesOnCartInput { input ShippingAddressInput { customer_address_id: Int # If provided then will be used address from address book address: CartAddressInput - cart_items: [CartItemQuantityInput!] -} - -input CartItemQuantityInput { - cart_item_id: Int! - quantity: Float! } input SetBillingAddressOnCartInput { @@ -100,10 +94,10 @@ input CartAddressInput { input SetShippingMethodsOnCartInput { cart_id: String! - shipping_methods: [ShippingMethodForAddressInput!]! + shipping_methods: [ShippingMethodInput!]! } -input ShippingMethodForAddressInput { +input ShippingMethodInput { cart_address_id: Int! carrier_code: String! method_code: String! @@ -117,10 +111,6 @@ input SetPaymentMethodOnCartInput { input PaymentMethodInput { code: String! @doc(description:"Payment method code") purchase_order_number: String @doc(description:"Purchase order number") - additional_data: PaymentMethodAdditionalDataInput -} - -input PaymentMethodAdditionalDataInput { } type SetPaymentMethodOnCartOutput { @@ -164,8 +154,8 @@ type CartAddress { country: CartAddressCountry telephone: String address_type: AdressTypeEnum - available_shipping_methods: [AvailableShippingMethod] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAdress\\AvailableShippingMethods") - selected_shipping_method: SelectedShippingMethod + available_shipping_methods: [AvailableShippingMethod] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddress\\AvailableShippingMethods") + selected_shipping_method: SelectedShippingMethod @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddress\\SelectedShippingMethod") items_weight: Float customer_notes: String cart_items: [CartItemQuantity] @@ -187,7 +177,10 @@ type CartAddressCountry { } type SelectedShippingMethod { - amount: Float! + carrier_code: String + method_code: String + label: String + amount: Float } type AvailableShippingMethod { @@ -210,10 +203,6 @@ type AvailablePaymentMethod { type SelectedPaymentMethod { code: String @doc(description: "The payment method code") purchase_order_number: String @doc(description: "The purchase order number.") - additional_data: SelectedPaymentMethodAdditionalData -} - -type SelectedPaymentMethodAdditionalData { } enum AdressTypeEnum { diff --git a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php index fd9adbe734101..da7ab97a1b211 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php @@ -445,7 +445,7 @@ public function getDateRange($range, $customStart, $customEnd, $returnObjects = break; case 'custom': - $dateStart = $customStart ? $customStart : $dateEnd; + $dateStart = $customStart ? $customStart : $dateStart; $dateEnd = $customEnd ? $customEnd : $dateEnd; break; diff --git a/app/code/Magento/Review/Observer/CatalogBlockProductCollectionBeforeToHtmlObserver.php b/app/code/Magento/Review/Observer/CatalogBlockProductCollectionBeforeToHtmlObserver.php index 6256194cef53b..f35d6eac27ea8 100644 --- a/app/code/Magento/Review/Observer/CatalogBlockProductCollectionBeforeToHtmlObserver.php +++ b/app/code/Magento/Review/Observer/CatalogBlockProductCollectionBeforeToHtmlObserver.php @@ -7,6 +7,9 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Review block observer. + */ class CatalogBlockProductCollectionBeforeToHtmlObserver implements ObserverInterface { /** @@ -35,7 +38,9 @@ public function execute(\Magento\Framework\Event\Observer $observer) { $productCollection = $observer->getEvent()->getCollection(); if ($productCollection instanceof \Magento\Framework\Data\Collection) { - $productCollection->load(); + if (!$productCollection->isLoaded()) { + $productCollection->load(); + } $this->_reviewFactory->create()->appendSummary($productCollection); } diff --git a/app/code/Magento/Robots/Model/Config/Value.php b/app/code/Magento/Robots/Model/Config/Value.php index c4e17e55f1262..16a5a486e1078 100644 --- a/app/code/Magento/Robots/Model/Config/Value.php +++ b/app/code/Magento/Robots/Model/Config/Value.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Robots\Model\Config; use Magento\Framework\App\Cache\TypeListInterface; @@ -32,12 +33,11 @@ class Value extends ConfigValue implements IdentityInterface const CACHE_TAG = 'robots'; /** - * Model cache tag for clear cache in after save and after delete + * @inheritdoc * - * @var string * @since 100.2.0 */ - protected $_cacheTag = true; + protected $_cacheTag = [self::CACHE_TAG]; /** * @var StoreManagerInterface diff --git a/app/code/Magento/Sales/Controller/Guest/Form.php b/app/code/Magento/Sales/Controller/Guest/Form.php index 8b4467cb538fa..04bb66f3d5b6e 100644 --- a/app/code/Magento/Sales/Controller/Guest/Form.php +++ b/app/code/Magento/Sales/Controller/Guest/Form.php @@ -4,40 +4,72 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Controller\Guest; -class Form extends \Magento\Framework\App\Action\Action +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Controller\Result\Redirect; +use Magento\Framework\View\Result\Page; +use Magento\Framework\View\Result\PageFactory; +use Magento\Sales\Helper\Guest as GuestHelper; + +/** + * Class Form + */ +class Form extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface { /** - * @var \Magento\Framework\View\Result\PageFactory + * @var PageFactory */ protected $resultPageFactory; /** - * @param \Magento\Framework\App\Action\Context $context - * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory + * @var CustomerSession|null + */ + private $customerSession; + + /** + * @var GuestHelper|null + */ + private $guestHelper; + + /** + * @param Context $context + * @param PageFactory $resultPageFactory + * @param CustomerSession|null $customerSession + * @param GuestHelper|null $guestHelper */ public function __construct( - \Magento\Framework\App\Action\Context $context, - \Magento\Framework\View\Result\PageFactory $resultPageFactory + Context $context, + PageFactory $resultPageFactory, + CustomerSession $customerSession = null, + GuestHelper $guestHelper = null ) { parent::__construct($context); $this->resultPageFactory = $resultPageFactory; + $this->customerSession = $customerSession ?: ObjectManager::getInstance()->get(CustomerSession::class); + $this->guestHelper = $guestHelper ?: ObjectManager::getInstance()->get(GuestHelper::class); } /** * Order view form page * - * @return \Magento\Framework\Controller\Result\Redirect|\Magento\Framework\View\Result\Page + * @return Redirect|Page */ public function execute() { - if ($this->_objectManager->get(\Magento\Customer\Model\Session::class)->isLoggedIn()) { + if ($this->customerSession->isLoggedIn()) { return $this->resultRedirectFactory->create()->setPath('customer/account/'); } + $resultPage = $this->resultPageFactory->create(); $resultPage->getConfig()->getTitle()->set(__('Orders and Returns')); - $this->_objectManager->get(\Magento\Sales\Helper\Guest::class)->getBreadcrumbs($resultPage); + $this->guestHelper->getBreadcrumbs($resultPage); + return $resultPage; } } diff --git a/app/code/Magento/SalesRule/Model/Validator.php b/app/code/Magento/SalesRule/Model/Validator.php index 5c0f97ae0b08b..ea0221d8f072d 100644 --- a/app/code/Magento/SalesRule/Model/Validator.php +++ b/app/code/Magento/SalesRule/Model/Validator.php @@ -182,6 +182,8 @@ protected function _getRules(Address $address = null) } /** + * Address id getter. + * * @param Address $address * @return string */ @@ -327,21 +329,7 @@ public function processShippingAmount(Address $address) $baseDiscountAmount = $rule->getDiscountAmount(); break; case \Magento\SalesRule\Model\Rule::CART_FIXED_ACTION: - $cartRules = $address->getCartFixedRules(); - if (!isset($cartRules[$rule->getId()])) { - $cartRules[$rule->getId()] = $rule->getDiscountAmount(); - } - if ($cartRules[$rule->getId()] > 0) { - $quoteAmount = $this->priceCurrency->convert($cartRules[$rule->getId()], $quote->getStore()); - $discountAmount = min($shippingAmount - $address->getShippingDiscountAmount(), $quoteAmount); - $baseDiscountAmount = min( - $baseShippingAmount - $address->getBaseShippingDiscountAmount(), - $cartRules[$rule->getId()] - ); - $cartRules[$rule->getId()] -= $baseDiscountAmount; - } - - $address->setCartFixedRules($cartRules); + // Shouldn't be proceed according to MAGETWO-96403 break; } @@ -519,6 +507,8 @@ public function sortItemsByPriority($items, Address $address = null) } /** + * Rule total items getter. + * * @param int $key * @return array * @throws \Magento\Framework\Exception\LocalizedException @@ -533,6 +523,8 @@ public function getRuleItemTotalsInfo($key) } /** + * Decrease rule items count. + * * @param int $key * @return $this */ diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php index 42448565791c5..e86068946ca78 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php @@ -5,6 +5,13 @@ */ namespace Magento\SalesRule\Test\Unit\Model; +use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Quote\Model\Quote; +use Magento\SalesRule\Model\Rule; +use Magento\SalesRule\Model\Validator; +use Magento\Store\Model\Store; +use PHPUnit\Framework\MockObject\MockObject; + /** * Class ValidatorTest * @@SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -17,50 +24,55 @@ class ValidatorTest extends \PHPUnit\Framework\TestCase protected $helper; /** - * @var \Magento\SalesRule\Model\Validator + * @var Validator */ protected $model; /** - * @var \Magento\Quote\Model\Quote\Item|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Quote\Model\Quote\Item|MockObject */ protected $item; /** - * @var \Magento\Quote\Model\Quote\Address|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Quote\Model\Quote\Address|MockObject */ protected $addressMock; /** - * @var \Magento\SalesRule\Model\RulesApplier|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\RulesApplier|MockObject */ protected $rulesApplier; /** - * @var \Magento\SalesRule\Model\Validator\Pool|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\Validator\Pool|MockObject */ protected $validators; /** - * @var \Magento\SalesRule\Model\Utility|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\Utility|MockObject */ protected $utility; /** - * @var \Magento\SalesRule\Model\ResourceModel\Rule\Collection|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\ResourceModel\Rule\Collection|MockObject */ protected $ruleCollection; /** - * @var \Magento\Catalog\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Catalog\Helper\Data|MockObject */ protected $catalogData; /** - * @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Message\ManagerInterface|MockObject */ protected $messageManager; + /** + * @var PriceCurrencyInterface|MockObject + */ + private $priceCurrency; + protected function setUp() { $this->helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -74,6 +86,7 @@ protected function setUp() ->setMethods( [ 'getShippingAmountForDiscount', + 'getBaseShippingAmountForDiscount', 'getQuote', 'getCustomAttributesCodes', 'setCartFixedRules' @@ -81,7 +94,7 @@ protected function setUp() ) ->getMock(); - /** @var \Magento\Quote\Model\Quote\Item\AbstractItem|\PHPUnit_Framework_MockObject_MockObject $item */ + /** @var \Magento\Quote\Model\Quote\Item\AbstractItem|MockObject $item */ $this->item = $this->createPartialMock( \Magento\Quote\Model\Quote\Item::class, ['__wakeup', 'getAddress', 'getParentItemId'] @@ -100,10 +113,13 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $ruleCollectionFactoryMock = $this->prepareRuleCollectionMock($this->ruleCollection); + $this->priceCurrency = $this->getMockBuilder(PriceCurrencyInterface::class) + ->disableOriginalConstructor() + ->getMock(); - /** @var \Magento\SalesRule\Model\Validator|\PHPUnit_Framework_MockObject_MockObject $validator */ + /** @var Validator|MockObject $validator */ $this->model = $this->helper->getObject( - \Magento\SalesRule\Model\Validator::class, + Validator::class, [ 'context' => $context, 'registry' => $registry, @@ -112,7 +128,8 @@ protected function setUp() 'utility' => $this->utility, 'rulesApplier' => $this->rulesApplier, 'validators' => $this->validators, - 'messageManager' => $this->messageManager + 'messageManager' => $this->messageManager, + 'priceCurrency' => $this->priceCurrency ] ); $this->model->setWebsiteId(1); @@ -131,7 +148,7 @@ protected function setUp() } /** - * @return \Magento\Quote\Model\Quote\Item|\PHPUnit_Framework_MockObject_MockObject + * @return \Magento\Quote\Model\Quote\Item|MockObject */ protected function getQuoteItemMock() { @@ -145,8 +162,8 @@ protected function getQuoteItemMock() $itemSimple = $this->createPartialMock(\Magento\Quote\Model\Quote\Item::class, ['getAddress', '__wakeup']); $itemSimple->expects($this->any())->method('getAddress')->will($this->returnValue($this->addressMock)); - /** @var $quote \Magento\Quote\Model\Quote */ - $quote = $this->createPartialMock(\Magento\Quote\Model\Quote::class, ['getStoreId', '__wakeup']); + /** @var $quote Quote */ + $quote = $this->createPartialMock(Quote::class, ['getStoreId', '__wakeup']); $quote->expects($this->any())->method('getStoreId')->will($this->returnValue(1)); $itemData = include $fixturePath . 'quote_item_downloadable.php'; @@ -168,7 +185,7 @@ public function testCanApplyRules() $this->model->getCouponCode() ); $item = $this->getQuoteItemMock(); - $rule = $this->createMock(\Magento\SalesRule\Model\Rule::class); + $rule = $this->createMock(Rule::class); $actionsCollection = $this->createPartialMock(\Magento\Rule\Model\Action\Collection::class, ['validate']); $actionsCollection->expects($this->any()) ->method('validate') @@ -278,7 +295,7 @@ public function testApplyRulesThatAppliedRuleIdsAreCollected() public function testInit() { $this->assertInstanceOf( - \Magento\SalesRule\Model\Validator::class, + Validator::class, $this->model->init( $this->model->getWebsiteId(), $this->model->getCustomerGroupId(), @@ -314,7 +331,7 @@ public function testCanApplyDiscount() public function testInitTotalsCanApplyDiscount() { $rule = $this->createPartialMock( - \Magento\SalesRule\Model\Rule::class, + Rule::class, ['getSimpleAction', 'getActions', 'getId'] ); $item1 = $this->getMockForAbstractClass( @@ -337,7 +354,7 @@ public function testInitTotalsCanApplyDiscount() $rule->expects($this->any()) ->method('getSimpleAction') - ->willReturn(\Magento\SalesRule\Model\Rule::CART_FIXED_ACTION); + ->willReturn(Rule::CART_FIXED_ACTION); $iterator = new \ArrayIterator([$rule]); $this->ruleCollection->expects($this->once())->method('getIterator')->willReturn($iterator); $validator = $this->getMockBuilder(\Magento\Framework\Validator\AbstractValidator::class) @@ -392,7 +409,7 @@ public function testInitTotalsNoItems() /** * @param $ruleCollection - * @return \PHPUnit_Framework_MockObject_MockObject + * @return MockObject */ protected function prepareRuleCollectionMock($ruleCollection) { @@ -427,14 +444,14 @@ public function testProcessShippingAmountNoRules() $this->model->getCouponCode() ); $this->assertInstanceOf( - \Magento\SalesRule\Model\Validator::class, + Validator::class, $this->model->processShippingAmount($this->setupAddressMock()) ); } public function testProcessShippingAmountProcessDisabled() { - $ruleMock = $this->getMockBuilder(\Magento\SalesRule\Model\Rule::class) + $ruleMock = $this->getMockBuilder(Rule::class) ->disableOriginalConstructor() ->setMethods([]) ->getMock(); @@ -448,51 +465,54 @@ public function testProcessShippingAmountProcessDisabled() $this->model->getCouponCode() ); $this->assertInstanceOf( - \Magento\SalesRule\Model\Validator::class, + Validator::class, $this->model->processShippingAmount($this->setupAddressMock()) ); } /** + * Tests shipping amounts according to rule simple action. + * * @param string $action + * @param int $ruleDiscount + * @param int $shippingDiscount * @dataProvider dataProviderActions */ - public function testProcessShippingAmountActions($action) + public function testProcessShippingAmountActions($action, $ruleDiscount, $shippingDiscount): void { - $discountAmount = 50; + $shippingAmount = 5; - $ruleMock = $this->getMockBuilder(\Magento\SalesRule\Model\Rule::class) + $ruleMock = $this->getMockBuilder(Rule::class) ->disableOriginalConstructor() ->setMethods(['getApplyToShipping', 'getSimpleAction', 'getDiscountAmount']) ->getMock(); - $ruleMock->expects($this->any()) - ->method('getApplyToShipping') + $ruleMock->method('getApplyToShipping') ->willReturn(true); - $ruleMock->expects($this->any()) - ->method('getDiscountAmount') - ->willReturn($discountAmount); - $ruleMock->expects($this->any()) - ->method('getSimpleAction') + $ruleMock->method('getDiscountAmount') + ->willReturn($ruleDiscount); + $ruleMock->method('getSimpleAction') ->willReturn($action); $iterator = new \ArrayIterator([$ruleMock]); - $this->ruleCollection->expects($this->any()) - ->method('getIterator') + $this->ruleCollection->method('getIterator') ->willReturn($iterator); - $this->utility->expects($this->any()) - ->method('canProcessRule') + $this->utility->method('canProcessRule') ->willReturn(true); + $this->priceCurrency->method('convert') + ->willReturn($ruleDiscount); + $this->model->init( $this->model->getWebsiteId(), $this->model->getCustomerGroupId(), $this->model->getCouponCode() ); - $this->assertInstanceOf( - \Magento\SalesRule\Model\Validator::class, - $this->model->processShippingAmount($this->setupAddressMock(5)) - ); + + $addressMock = $this->setupAddressMock($shippingAmount); + + self::assertInstanceOf(Validator::class, $this->model->processShippingAmount($addressMock)); + self::assertEquals($shippingDiscount, $addressMock->getShippingDiscountAmount()); } /** @@ -501,44 +521,48 @@ public function testProcessShippingAmountActions($action) public static function dataProviderActions() { return [ - [\Magento\SalesRule\Model\Rule::TO_PERCENT_ACTION], - [\Magento\SalesRule\Model\Rule::BY_PERCENT_ACTION], - [\Magento\SalesRule\Model\Rule::TO_FIXED_ACTION], - [\Magento\SalesRule\Model\Rule::BY_FIXED_ACTION], - [\Magento\SalesRule\Model\Rule::CART_FIXED_ACTION], + [Rule::TO_PERCENT_ACTION, 50, 2.5], + [Rule::BY_PERCENT_ACTION, 50, 2.5], + [Rule::TO_FIXED_ACTION, 5, 0], + [Rule::BY_FIXED_ACTION, 5, 5], + [Rule::CART_FIXED_ACTION, 5, 0], ]; } /** * @param null|int $shippingAmount - * @return \PHPUnit_Framework_MockObject_MockObject + * @return MockObject */ protected function setupAddressMock($shippingAmount = null) { - $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) + $storeMock = $this->getMockBuilder(Store::class) ->disableOriginalConstructor() ->setMethods([]) ->getMock(); - $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) + + $quoteMock = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() ->setMethods(['setAppliedRuleIds', 'getStore']) ->getMock(); - $quoteMock->expects($this->any()) - ->method('getStore') + + $quoteMock->method('getStore') ->willReturn($storeMock); - $quoteMock->expects($this->any()) - ->method('setAppliedRuleIds') + + $quoteMock->method('setAppliedRuleIds') ->willReturnSelf(); - $this->addressMock->expects($this->any()) - ->method('getShippingAmountForDiscount') + $this->addressMock->method('getShippingAmountForDiscount') ->willReturn($shippingAmount); - $this->addressMock->expects($this->any()) - ->method('getQuote') + + $this->addressMock->method('getBaseShippingAmountForDiscount') + ->willReturn($shippingAmount); + + $this->addressMock->method('getQuote') ->willReturn($quoteMock); - $this->addressMock->expects($this->any()) - ->method('getCustomAttributesCodes') + + $this->addressMock->method('getCustomAttributesCodes') ->willReturn([]); + return $this->addressMock; } @@ -546,7 +570,7 @@ public function testReset() { $this->utility->expects($this->once()) ->method('resetRoundingDeltas'); - $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) + $quoteMock = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() ->getMock(); $addressMock = $this->getMockBuilder(\Magento\Quote\Model\Quote\Address::class) @@ -560,6 +584,6 @@ public function testReset() $this->model->getCustomerGroupId(), $this->model->getCouponCode() ); - $this->assertInstanceOf(\Magento\SalesRule\Model\Validator::class, $this->model->reset($addressMock)); + $this->assertInstanceOf(Validator::class, $this->model->reset($addressMock)); } } diff --git a/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml b/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml index 9b579f47759a6..570eb0bf151f0 100644 --- a/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml +++ b/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml @@ -452,7 +452,7 @@ <dataScope>discount_step</dataScope> </settings> </field> - <field name="apply_to_shipping" component="Magento_Ui/js/form/element/single-checkbox-toggle-notice" formElement="checkbox"> + <field name="apply_to_shipping" component="Magento_SalesRule/js/form/element/apply_to_shipping" formElement="checkbox"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="source" xsi:type="string">sales_rule</item> diff --git a/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js b/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js new file mode 100644 index 0000000000000..dfb3f909345b3 --- /dev/null +++ b/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js @@ -0,0 +1,37 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/form/element/single-checkbox-toggle-notice' +], function (Checkbox) { + 'use strict'; + + return Checkbox.extend({ + defaults: { + imports: { + toggleDisabled: '${ $.parentName }.simple_action:value' + } + }, + + /** + * Toggle element disabled state according to simple action value. + * + * @param {String} action + */ + toggleDisabled: function (action) { + switch (action) { + case 'cart_fixed': + this.disabled(true); + break; + default: + this.disabled(false); + } + + if (this.disabled()) { + this.checked(false); + } + } + }); +}); diff --git a/app/code/Magento/Sitemap/Model/Sitemap.php b/app/code/Magento/Sitemap/Model/Sitemap.php index d58ff732c81d7..c35e20d997d85 100644 --- a/app/code/Magento/Sitemap/Model/Sitemap.php +++ b/app/code/Magento/Sitemap/Model/Sitemap.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sitemap\Model; use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot; @@ -15,6 +16,8 @@ use Magento\Sitemap\Model\ResourceModel\Sitemap as SitemapResource; /** + * Sitemap model. + * * @method string getSitemapType() * @method \Magento\Sitemap\Model\Sitemap setSitemapType(string $value) * @method string getSitemapFilename() @@ -154,12 +157,11 @@ class Sitemap extends \Magento\Framework\Model\AbstractModel implements \Magento protected $dateTime; /** - * Model cache tag for clear cache in after save and after delete + * @inheritdoc * - * @var string * @since 100.2.0 */ - protected $_cacheTag = true; + protected $_cacheTag = [Value::CACHE_TAG]; /** * Item resolver diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml index efd3a8c6b8cad..d30fc1e5a2a35 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml @@ -17,4 +17,9 @@ <click selector="{{StorefrontHeaderSection.storeViewOption(storeView.code)}}" stepKey="clickSelectStoreView"/> <waitForPageLoad stepKey="waitForPageLoad"/> </actionGroup> + + <actionGroup name="StorefrontSwitchDefaultStoreViewActionGroup" extends="StorefrontSwitchStoreViewActionGroup"> + <remove keyForRemoval="clickSelectStoreView"/> + <click selector="{{StorefrontHeaderSection.storeViewOption('default')}}" stepKey="clickSelectDefaultStoreView" after="waitForStoreViewDropdown"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Tax/Plugin/Checkout/CustomerData/Cart.php b/app/code/Magento/Tax/Plugin/Checkout/CustomerData/Cart.php index 87f65ef311ac2..208833733ae3f 100644 --- a/app/code/Magento/Tax/Plugin/Checkout/CustomerData/Cart.php +++ b/app/code/Magento/Tax/Plugin/Checkout/CustomerData/Cart.php @@ -6,6 +6,10 @@ namespace Magento\Tax\Plugin\Checkout\CustomerData; +/** + * Process quote items price, considering tax configuration. + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class Cart { /** @@ -68,6 +72,16 @@ public function afterGetSectionData(\Magento\Checkout\CustomerData\Cart $subject $this->itemPriceRenderer->setItem($item); $this->itemPriceRenderer->setTemplate('checkout/cart/item/price/sidebar.phtml'); $result['items'][$key]['product_price']=$this->itemPriceRenderer->toHtml(); + if ($this->itemPriceRenderer->displayPriceExclTax()) { + $result['items'][$key]['product_price_value'] = $item->getCalculationPrice(); + } elseif ($this->itemPriceRenderer->displayPriceInclTax()) { + $result['items'][$key]['product_price_value'] = $item->getPriceInclTax(); + } elseif ($this->itemPriceRenderer->displayBothPrices()) { + //unset product price value in case price already has been set as scalar value. + unset($result['items'][$key]['product_price_value']); + $result['items'][$key]['product_price_value']['incl_tax'] = $item->getPriceInclTax(); + $result['items'][$key]['product_price_value']['excl_tax'] = $item->getCalculationPrice(); + } } } } diff --git a/app/code/Magento/Ui/view/base/web/js/form/components/button.js b/app/code/Magento/Ui/view/base/web/js/form/components/button.js index df85af5824d92..bfc2eb2b8852b 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/components/button.js +++ b/app/code/Magento/Ui/view/base/web/js/form/components/button.js @@ -45,7 +45,8 @@ define([ .observe([ 'visible', 'disabled', - 'title' + 'title', + 'childError' ]); }, diff --git a/app/code/Magento/Ui/view/base/web/js/lib/validation/validator.js b/app/code/Magento/Ui/view/base/web/js/lib/validation/validator.js index be8fd2ce9fcef..8ebbf88775b86 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/validation/validator.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/validation/validator.js @@ -48,6 +48,10 @@ define([ params : [params]; + if (typeof message === 'function') { + message = message.call(rule); + } + message = params.reduce(function (msg, param, idx) { return msg.replace(new RegExp('\\{' + idx + '\\}', 'g'), param); }, message); diff --git a/app/code/Magento/Ui/view/base/web/templates/form/element/button.html b/app/code/Magento/Ui/view/base/web/templates/form/element/button.html index a8b6c3a858956..766df35f5764b 100644 --- a/app/code/Magento/Ui/view/base/web/templates/form/element/button.html +++ b/app/code/Magento/Ui/view/base/web/templates/form/element/button.html @@ -11,3 +11,15 @@ attr="'data-index': index"> <span text="title"/> </button> + +<if args="childError"> + <strong class="_error"> + <span class="admin__page-nav-item-messages"> + <span class="admin__page-nav-item-message _error"> + <span class="admin__page-nav-item-message-icon"></span> + <span class="admin__page-nav-item-message-tooltip" + data-bind="i18n: 'This element contains invalid data. Please resolve this before saving.'">This element contains invalid data. Please resolve this before saving.</span> + </span> + </span> + </strong> +</if> diff --git a/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php b/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php index 2ec573b6459da..e1bb094e7fc39 100644 --- a/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php +++ b/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php @@ -81,7 +81,12 @@ public function switch(StoreInterface $fromStore, StoreInterface $targetStore, s $existingRewrite = $this->urlFinder->findOneByData([ UrlRewrite::REQUEST_PATH => $urlPath ]); - if ($existingRewrite) { + $currentRewrite = $this->urlFinder->findOneByData([ + UrlRewrite::REQUEST_PATH => $urlPath, + UrlRewrite::STORE_ID => $targetStore->getId(), + ]); + + if ($existingRewrite && !$currentRewrite) { /** @var \Magento\Framework\App\Response\Http $response */ $targetUrl = $targetStore->getBaseUrl(); } diff --git a/app/code/Magento/Wishlist/Controller/Index/Allcart.php b/app/code/Magento/Wishlist/Controller/Index/Allcart.php index 8463a00c866c5..958e0f49ca1b6 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Allcart.php +++ b/app/code/Magento/Wishlist/Controller/Index/Allcart.php @@ -5,14 +5,17 @@ */ namespace Magento\Wishlist\Controller\Index; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Data\Form\FormKey\Validator; -use Magento\Framework\App\Action; use Magento\Framework\App\Action\Context; use Magento\Wishlist\Controller\WishlistProviderInterface; use Magento\Wishlist\Model\ItemCarrier; use Magento\Framework\Controller\ResultFactory; -class Allcart extends \Magento\Wishlist\Controller\AbstractIndex +/** + * Action Add All to Cart + */ +class Allcart extends \Magento\Wishlist\Controller\AbstractIndex implements HttpPostActionInterface { /** * @var WishlistProviderInterface diff --git a/composer.lock b/composer.lock index 84e89277a95db..72d3e93a7b35e 100644 --- a/composer.lock +++ b/composer.lock @@ -257,16 +257,16 @@ }, { "name": "composer/composer", - "version": "1.8.3", + "version": "1.8.4", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "a6a3b44581398b7135c7baa0557b7c5b10808b47" + "reference": "bc364c2480c17941e2135cfc568fa41794392534" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/a6a3b44581398b7135c7baa0557b7c5b10808b47", - "reference": "a6a3b44581398b7135c7baa0557b7c5b10808b47", + "url": "https://api.github.com/repos/composer/composer/zipball/bc364c2480c17941e2135cfc568fa41794392534", + "reference": "bc364c2480c17941e2135cfc568fa41794392534", "shasum": "" }, "require": { @@ -333,7 +333,7 @@ "dependency", "package" ], - "time": "2019-01-30T07:31:34+00:00" + "time": "2019-02-11T09:52:10+00:00" }, { "name": "composer/semver", @@ -1912,7 +1912,7 @@ }, { "name": "symfony/css-selector", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -2028,16 +2028,16 @@ }, { "name": "symfony/filesystem", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "7c16ebc2629827d4ec915a52ac809768d060a4ee" + "reference": "e16b9e471703b2c60b95f14d31c1239f68f11601" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/7c16ebc2629827d4ec915a52ac809768d060a4ee", - "reference": "7c16ebc2629827d4ec915a52ac809768d060a4ee", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/e16b9e471703b2c60b95f14d31c1239f68f11601", + "reference": "e16b9e471703b2c60b95f14d31c1239f68f11601", "shasum": "" }, "require": { @@ -2074,20 +2074,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2019-01-16T20:35:37+00:00" + "time": "2019-02-07T11:40:08+00:00" }, { "name": "symfony/finder", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "ef71816cbb264988bb57fe6a73f610888b9aa70c" + "reference": "267b7002c1b70ea80db0833c3afe05f0fbde580a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ef71816cbb264988bb57fe6a73f610888b9aa70c", - "reference": "ef71816cbb264988bb57fe6a73f610888b9aa70c", + "url": "https://api.github.com/repos/symfony/finder/zipball/267b7002c1b70ea80db0833c3afe05f0fbde580a", + "reference": "267b7002c1b70ea80db0833c3afe05f0fbde580a", "shasum": "" }, "require": { @@ -2123,7 +2123,7 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2019-01-16T20:35:37+00:00" + "time": "2019-02-23T15:42:05+00:00" }, { "name": "symfony/polyfill-ctype", @@ -2753,16 +2753,16 @@ }, { "name": "zendframework/zend-db", - "version": "2.9.3", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-db.git", - "reference": "5b4f2c42f94c9f7f4b2f456a0ebe459fab12b3d9" + "reference": "77022f06f6ffd384fa86d22ab8d8bbdb925a1e8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-db/zipball/5b4f2c42f94c9f7f4b2f456a0ebe459fab12b3d9", - "reference": "5b4f2c42f94c9f7f4b2f456a0ebe459fab12b3d9", + "url": "https://api.github.com/repos/zendframework/zend-db/zipball/77022f06f6ffd384fa86d22ab8d8bbdb925a1e8e", + "reference": "77022f06f6ffd384fa86d22ab8d8bbdb925a1e8e", "shasum": "" }, "require": { @@ -2773,7 +2773,7 @@ "phpunit/phpunit": "^5.7.25 || ^6.4.4", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", - "zendframework/zend-hydrator": "^1.1 || ^2.1", + "zendframework/zend-hydrator": "^1.1 || ^2.1 || ^3.0", "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3" }, "suggest": { @@ -2807,7 +2807,7 @@ "db", "zf" ], - "time": "2018-04-09T13:21:36+00:00" + "time": "2019-02-25T11:37:45+00:00" }, { "name": "zendframework/zend-di", @@ -4377,16 +4377,16 @@ }, { "name": "zendframework/zend-uri", - "version": "2.6.1", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-uri.git", - "reference": "3b6463645c6766f78ce537c70cb4fdabee1e725f" + "reference": "b2785cd38fe379a784645449db86f21b7739b1ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-uri/zipball/3b6463645c6766f78ce537c70cb4fdabee1e725f", - "reference": "3b6463645c6766f78ce537c70cb4fdabee1e725f", + "url": "https://api.github.com/repos/zendframework/zend-uri/zipball/b2785cd38fe379a784645449db86f21b7739b1ee", + "reference": "b2785cd38fe379a784645449db86f21b7739b1ee", "shasum": "" }, "require": { @@ -4401,8 +4401,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.6.x-dev", - "dev-develop": "2.7.x-dev" + "dev-master": "2.7.x-dev", + "dev-develop": "2.8.x-dev" } }, "autoload": { @@ -4420,7 +4420,7 @@ "uri", "zf" ], - "time": "2018-04-30T13:40:08+00:00" + "time": "2019-02-27T21:39:04+00:00" }, { "name": "zendframework/zend-validator", @@ -4889,16 +4889,16 @@ }, { "name": "codeception/phpunit-wrapper", - "version": "6.5.1", + "version": "6.6.1", "source": { "type": "git", "url": "https://github.com/Codeception/phpunit-wrapper.git", - "reference": "d78f9eb9c4300a5924cc27dee03e8c1a96fcf5f3" + "reference": "d0da25a98bcebeb15d97c2ad3b2de6166b6e7a0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/d78f9eb9c4300a5924cc27dee03e8c1a96fcf5f3", - "reference": "d78f9eb9c4300a5924cc27dee03e8c1a96fcf5f3", + "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/d0da25a98bcebeb15d97c2ad3b2de6166b6e7a0c", + "reference": "d0da25a98bcebeb15d97c2ad3b2de6166b6e7a0c", "shasum": "" }, "require": { @@ -4912,7 +4912,7 @@ }, "require-dev": { "codeception/specify": "*", - "vlucas/phpdotenv": "^2.4" + "vlucas/phpdotenv": "^3.0" }, "type": "library", "autoload": { @@ -4931,24 +4931,24 @@ } ], "description": "PHPUnit classes used by Codeception", - "time": "2019-01-13T10:34:55+00:00" + "time": "2019-02-26T20:47:39+00:00" }, { "name": "codeception/stub", - "version": "2.0.4", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/Codeception/Stub.git", - "reference": "f50bc271f392a2836ff80690ce0c058efe1ae03e" + "reference": "853657f988942f7afb69becf3fd0059f192c705a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/f50bc271f392a2836ff80690ce0c058efe1ae03e", - "reference": "f50bc271f392a2836ff80690ce0c058efe1ae03e", + "url": "https://api.github.com/repos/Codeception/Stub/zipball/853657f988942f7afb69becf3fd0059f192c705a", + "reference": "853657f988942f7afb69becf3fd0059f192c705a", "shasum": "" }, "require": { - "phpunit/phpunit": ">=4.8 <8.0" + "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3" }, "type": "library", "autoload": { @@ -4961,7 +4961,7 @@ "MIT" ], "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", - "time": "2018-07-26T11:55:37+00:00" + "time": "2019-03-02T15:35:10+00:00" }, { "name": "consolidation/annotated-command", @@ -5061,16 +5061,16 @@ }, { "name": "consolidation/config", - "version": "1.1.1", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/consolidation/config.git", - "reference": "925231dfff32f05b787e1fddb265e789b939cf4c" + "reference": "cac1279bae7efb5c7fb2ca4c3ba4b8eb741a96c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/config/zipball/925231dfff32f05b787e1fddb265e789b939cf4c", - "reference": "925231dfff32f05b787e1fddb265e789b939cf4c", + "url": "https://api.github.com/repos/consolidation/config/zipball/cac1279bae7efb5c7fb2ca4c3ba4b8eb741a96c1", + "reference": "cac1279bae7efb5c7fb2ca4c3ba4b8eb741a96c1", "shasum": "" }, "require": { @@ -5079,9 +5079,9 @@ "php": ">=5.4.0" }, "require-dev": { - "g1a/composer-test-scenarios": "^1", + "g1a/composer-test-scenarios": "^3", + "php-coveralls/php-coveralls": "^1", "phpunit/phpunit": "^5", - "satooshi/php-coveralls": "^1.0", "squizlabs/php_codesniffer": "2.*", "symfony/console": "^2.5|^3|^4", "symfony/yaml": "^2.8.11|^3|^4" @@ -5091,6 +5091,33 @@ }, "type": "library", "extra": { + "scenarios": { + "symfony4": { + "require-dev": { + "symfony/console": "^4.0" + }, + "config": { + "platform": { + "php": "7.1.3" + } + } + }, + "symfony2": { + "require-dev": { + "symfony/console": "^2.8", + "symfony/event-dispatcher": "^2.8", + "phpunit/phpunit": "^4.8.36" + }, + "remove": [ + "php-coveralls/php-coveralls" + ], + "config": { + "platform": { + "php": "5.4.8" + } + } + } + }, "branch-alias": { "dev-master": "1.x-dev" } @@ -5111,7 +5138,7 @@ } ], "description": "Provide configuration services for a commandline tool.", - "time": "2018-10-24T17:55:35+00:00" + "time": "2019-03-03T19:37:04+00:00" }, { "name": "consolidation/log", @@ -5261,16 +5288,16 @@ }, { "name": "consolidation/robo", - "version": "1.4.4", + "version": "1.4.6", "source": { "type": "git", "url": "https://github.com/consolidation/Robo.git", - "reference": "8bec6a6ea54a7d03d56552a4250c49dec3b3083d" + "reference": "d4805a1abbc730e9a6d64ede2eba56f91a2b4eb3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/Robo/zipball/8bec6a6ea54a7d03d56552a4250c49dec3b3083d", - "reference": "8bec6a6ea54a7d03d56552a4250c49dec3b3083d", + "url": "https://api.github.com/repos/consolidation/Robo/zipball/d4805a1abbc730e9a6d64ede2eba56f91a2b4eb3", + "reference": "d4805a1abbc730e9a6d64ede2eba56f91a2b4eb3", "shasum": "" }, "require": { @@ -5365,7 +5392,7 @@ } ], "description": "Modern task runner", - "time": "2019-02-08T20:59:23+00:00" + "time": "2019-02-17T05:32:27+00:00" }, { "name": "consolidation/self-update", @@ -7815,7 +7842,6 @@ "mock", "xunit" ], - "abandoned": true, "time": "2018-08-09T05:50:03+00:00" }, { @@ -8559,16 +8585,16 @@ }, { "name": "symfony/browser-kit", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "ee4462581eb54bf34b746e4a5d522a4f21620160" + "reference": "61d85c5af2fc058014c7c89504c3944e73a086f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/ee4462581eb54bf34b746e4a5d522a4f21620160", - "reference": "ee4462581eb54bf34b746e4a5d522a4f21620160", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/61d85c5af2fc058014c7c89504c3944e73a086f0", + "reference": "61d85c5af2fc058014c7c89504c3944e73a086f0", "shasum": "" }, "require": { @@ -8612,20 +8638,20 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "time": "2019-01-16T21:31:25+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/config", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "25a2e7abe0d97e70282537292e3df45cf6da7b98" + "reference": "7f70d79c7a24a94f8e98abb988049403a53d7b31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/25a2e7abe0d97e70282537292e3df45cf6da7b98", - "reference": "25a2e7abe0d97e70282537292e3df45cf6da7b98", + "url": "https://api.github.com/repos/symfony/config/zipball/7f70d79c7a24a94f8e98abb988049403a53d7b31", + "reference": "7f70d79c7a24a94f8e98abb988049403a53d7b31", "shasum": "" }, "require": { @@ -8675,7 +8701,7 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2019-01-30T11:44:30+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/contracts", @@ -8747,16 +8773,16 @@ }, { "name": "symfony/dependency-injection", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "72c14cbc0c27706b9b4c33b9cd7a280972ff4806" + "reference": "cdadb3765df7c89ac93628743913b92bb91f1704" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/72c14cbc0c27706b9b4c33b9cd7a280972ff4806", - "reference": "72c14cbc0c27706b9b4c33b9cd7a280972ff4806", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/cdadb3765df7c89ac93628743913b92bb91f1704", + "reference": "cdadb3765df7c89ac93628743913b92bb91f1704", "shasum": "" }, "require": { @@ -8816,20 +8842,20 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2019-01-30T17:51:38+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/dom-crawler", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "d8476760b04cdf7b499c8718aa437c20a9155103" + "reference": "53c97769814c80a84a8403efcf3ae7ae966d53bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/d8476760b04cdf7b499c8718aa437c20a9155103", - "reference": "d8476760b04cdf7b499c8718aa437c20a9155103", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/53c97769814c80a84a8403efcf3ae7ae966d53bb", + "reference": "53c97769814c80a84a8403efcf3ae7ae966d53bb", "shasum": "" }, "require": { @@ -8873,20 +8899,20 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2019-01-16T20:35:37+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/http-foundation", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "8d2318b73e0a1bc75baa699d00ebe2ae8b595a39" + "reference": "850a667d6254ccf6c61d853407b16f21c4579c77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/8d2318b73e0a1bc75baa699d00ebe2ae8b595a39", - "reference": "8d2318b73e0a1bc75baa699d00ebe2ae8b595a39", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/850a667d6254ccf6c61d853407b16f21c4579c77", + "reference": "850a667d6254ccf6c61d853407b16f21c4579c77", "shasum": "" }, "require": { @@ -8927,20 +8953,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2019-01-29T09:49:29+00:00" + "time": "2019-02-26T08:03:39+00:00" }, { "name": "symfony/options-resolver", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "831b272963a8aa5a0613a1a7f013322d8161bbbb" + "reference": "3896e5a7d06fd15fa4947694c8dcdd371ff147d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/831b272963a8aa5a0613a1a7f013322d8161bbbb", - "reference": "831b272963a8aa5a0613a1a7f013322d8161bbbb", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/3896e5a7d06fd15fa4947694c8dcdd371ff147d1", + "reference": "3896e5a7d06fd15fa4947694c8dcdd371ff147d1", "shasum": "" }, "require": { @@ -8981,7 +9007,7 @@ "configuration", "options" ], - "time": "2019-01-16T21:31:25+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/polyfill-php70", @@ -9099,7 +9125,7 @@ }, { "name": "symfony/stopwatch", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -9149,16 +9175,16 @@ }, { "name": "symfony/yaml", - "version": "v3.4.22", + "version": "v3.4.23", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "ba11776e9e6c15ad5759a07bffb15899bac75c2d" + "reference": "57f1ce82c997f5a8701b89ef970e36bb657fd09c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/ba11776e9e6c15ad5759a07bffb15899bac75c2d", - "reference": "ba11776e9e6c15ad5759a07bffb15899bac75c2d", + "url": "https://api.github.com/repos/symfony/yaml/zipball/57f1ce82c997f5a8701b89ef970e36bb657fd09c", + "reference": "57f1ce82c997f5a8701b89ef970e36bb657fd09c", "shasum": "" }, "require": { @@ -9204,7 +9230,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2019-01-16T10:59:17+00:00" + "time": "2019-02-23T15:06:07+00:00" }, { "name": "theseer/fdomdocument", diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php index a7c83aba89f0a..e517b22ad09de 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php @@ -137,6 +137,26 @@ public function testQueryAllFieldsSimpleProduct() sort_order } } + ... on CustomizableCheckboxOption { + checkbox_option: value { + option_type_id + sku + price + price_type + title + sort_order + } + } + ... on CustomizableMultipleOption { + multiple_option: value { + option_type_id + sku + price + price_type + title + sort_order + } + } ...on CustomizableFileOption { product_sku file_option: value { @@ -736,7 +756,7 @@ private function assertOptions($product, $actualResponse) $values = $option->getValues(); /** @var \Magento\Catalog\Model\Product\Option\Value $value */ $value = current($values); - $findValueKeyName = $option->getType() === 'radio' ? 'radio_option' : 'drop_down_option'; + $findValueKeyName = $option->getType() . '_option'; if ($value->getTitle() === $optionsArray[$findValueKeyName][0]['title']) { $match = true; } @@ -756,7 +776,7 @@ private function assertOptions($product, $actualResponse) ]; if (!empty($option->getValues())) { - $valueKeyName = $option->getType() === 'radio' ? 'radio_option' : 'drop_down_option'; + $valueKeyName = $option->getType() . '_option'; $value = current($optionsArray[$valueKeyName]); /** @var \Magento\Catalog\Model\Product\Option\Value $productValue */ $productValue = current($option->getValues()); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php new file mode 100644 index 0000000000000..17c2af8dc59d0 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogInventory; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; + +class AddProductToCartTest extends GraphQlAbstract +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @expectedException \Exception + * @expectedExceptionMessage The requested qty is not available + */ + public function testAddProductIfQuantityIsNotAvailable() + { + $sku = 'simple'; + $qty = 200; + + $maskedQuoteId = $this->getMaskedQuoteId(); + $query = $this->getAddSimpleProductQuery($maskedQuoteId, $sku, $qty); + $this->graphQlQuery($query); + self::fail('Should be "The requested qty is not available" error message.'); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @magentoConfigFixture default cataloginventory/item_options/max_sale_qty 5 + * @expectedException \Exception + * @expectedExceptionMessage The most you may purchase is 5. + */ + public function testAddMoreProductsThatAllowed() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/167'); + + $sku = 'custom-design-simple-product'; + $qty = 7; + + $maskedQuoteId = $this->getMaskedQuoteId(); + $query = $this->getAddSimpleProductQuery($maskedQuoteId, $sku, $qty); + $this->graphQlQuery($query); + self::fail('Should be "The most you may purchase is 5." error message.'); + } + + /** + * @return string + */ + public function getMaskedQuoteId() : string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_order_1', 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } + + /** + * @param string $maskedQuoteId + * @param string $sku + * @param int $qty + * @return string + */ + public function getAddSimpleProductQuery(string $maskedQuoteId, string $sku, int $qty) : string + { + return <<<QUERY +mutation { + addSimpleProductsToCart( + input: { + cart_id: "{$maskedQuoteId}", + cartItems: [ + { + data: { + qty: $qty + sku: "$sku" + } + } + ] + } + ) { + cart { + items { + qty + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartTest.php index d5bdb942e9b2c..3bda57381e9d1 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartTest.php @@ -9,7 +9,7 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; -use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteFactory; use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; @@ -21,9 +21,9 @@ class AddSimpleProductToCartTest extends GraphQlAbstract private $quoteResource; /** - * @var Quote + * @var QuoteFactory */ - private $quote; + private $quoteFactory; /** * @var QuoteIdToMaskedQuoteIdInterface @@ -37,29 +37,48 @@ protected function setUp() { $objectManager = Bootstrap::getObjectManager(); $this->quoteResource = $objectManager->get(QuoteResource::class); - $this->quote = $objectManager->create(Quote::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); } /** * @magentoApiDataFixture Magento/Catalog/_files/products.php * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php - * @expectedException \Exception - * @expectedExceptionMessage The requested qty is not available */ - public function testAddProductIfQuantityIsNotAvailable() + public function testAddSimpleProductToCart() { $sku = 'simple'; - $qty = 200; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteId(); - $this->quoteResource->load( - $this->quote, - 'test_order_1', - 'reserved_order_id' - ); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + $query = $this->getAddSimpleProductQuery($maskedQuoteId, $sku, $qty); + $response = $this->graphQlQuery($query); + self::assertArrayHasKey('cart', $response['addSimpleProductsToCart']); - $query = <<<QUERY + self::assertEquals($qty, $response['addSimpleProductsToCart']['cart']['items'][0]['qty']); + self::assertEquals($sku, $response['addSimpleProductsToCart']['cart']['items'][0]['product']['sku']); + } + + /** + * @return string + */ + public function getMaskedQuoteId() : string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_order_1', 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } + + /** + * @param string $maskedQuoteId + * @param string $sku + * @param int $qty + * @return string + */ + public function getAddSimpleProductQuery(string $maskedQuoteId, string $sku, int $qty): string + { + return <<<QUERY mutation { addSimpleProductsToCart( input: { @@ -77,12 +96,13 @@ public function testAddProductIfQuantityIsNotAvailable() cart { items { qty + product { + sku + } } } } } QUERY; - - $this->graphQlQuery($query); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php index 96f32d781267b..1a93c011e80a8 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php @@ -90,6 +90,7 @@ public function testSetNewBillingAddress() code label } + address_type } } } @@ -147,6 +148,7 @@ public function testSetNewBillingAddressWithUseForShippingParameter() code label } + address_type } shipping_addresses { firstname @@ -160,6 +162,7 @@ public function testSetNewBillingAddressWithUseForShippingParameter() code label } + address_type } } } @@ -174,7 +177,7 @@ public function testSetNewBillingAddressWithUseForShippingParameter() self::assertArrayHasKey('shipping_addresses', $cartResponse); $shippingAddressResponse = current($cartResponse['shipping_addresses']); $this->assertNewAddressFields($billingAddressResponse); - $this->assertNewAddressFields($shippingAddressResponse); + $this->assertNewAddressFields($shippingAddressResponse, 'SHIPPING'); } /** @@ -403,9 +406,10 @@ public function testSetBillingAddressIfCustomerIsNotOwnerOfAddress() /** * Verify the all the whitelisted fields for a New Address Object * - * @param array $billingAddressResponse + * @param array $addressResponse + * @param string $addressType */ - private function assertNewAddressFields(array $billingAddressResponse): void + private function assertNewAddressFields(array $addressResponse, string $addressType = 'BILLING'): void { $assertionMap = [ ['response_field' => 'firstname', 'expected_value' => 'test firstname'], @@ -416,9 +420,10 @@ private function assertNewAddressFields(array $billingAddressResponse): void ['response_field' => 'postcode', 'expected_value' => '887766'], ['response_field' => 'telephone', 'expected_value' => '88776655'], ['response_field' => 'country', 'expected_value' => ['code' => 'US', 'label' => 'US']], + ['response_field' => 'address_type', 'expected_value' => $addressType] ]; - $this->assertResponseFields($billingAddressResponse, $assertionMap); + $this->assertResponseFields($addressResponse, $assertionMap); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php new file mode 100644 index 0000000000000..736ea69440753 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php @@ -0,0 +1,206 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting shipping methods on cart for customer + */ +class SetShippingMethodsOnCartTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + public function testShippingMethodWithVirtualProduct() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testShippingMethodWithSimpleProduct() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testShippingMethodWithSimpleProductWithoutAddress() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetShippingMethodWithMissedRequiredParameters() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetNonExistentShippingMethod() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetShippingMethodIfAddressIsNotBelongToCart() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetShippingMethodToNonExistentCart() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetShippingMethodToGuestCart() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetShippingMethodToAnotherCustomerCart() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetShippingMethodToNonExistentCartAddress() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetShippingMethodToGuestCartAddress() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetShippingMethodToAnotherCustomerCartAddress() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + public function testSetMultipleShippingMethods() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/423'); + } + + /** + * @param string $maskedQuoteId + * @param string $shippingMethodCode + * @param string $shippingCarrierCode + * @param string $shippingAddressId + * @return string + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + private function prepareMutationQuery( + string $maskedQuoteId, + string $shippingMethodCode, + string $shippingCarrierCode, + string $shippingAddressId + ) : string { + return <<<QUERY +mutation { + setShippingMethodsOnCart(input: + { + cart_id: "$maskedQuoteId", + shipping_addresses: [{ + cart_address_id: $shippingAddressId + shipping_method: { + method_code: "$shippingMethodCode" + carrier_code: "$shippingCarrierCode" + } + }] + }) { + + cart { + cart_id, + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + label + amount + } + } + } + } +} + +QUERY; + } + + /** + * @param string $reversedQuoteId + * @return string + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } + + /** + * @param string $reversedQuoteId + * @param int $customerId + * @return string + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + private function assignQuoteToCustomer( + string $reversedQuoteId, + int $customerId + ): string { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + $quote->setCustomerId($customerId); + $this->quoteResource->save($quote); + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } + + /** + * @param string $username + * @param string $password + * @return array + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php index 24fc8353d2552..880d6aa0f406f 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php @@ -82,6 +82,7 @@ public function testSetNewBillingAddress() code label } + address_type } } } @@ -138,6 +139,7 @@ public function testSetNewBillingAddressWithUseForShippingParameter() code label } + address_type } shipping_addresses { firstname @@ -151,6 +153,7 @@ public function testSetNewBillingAddressWithUseForShippingParameter() code label } + address_type } } } @@ -165,7 +168,7 @@ public function testSetNewBillingAddressWithUseForShippingParameter() self::assertArrayHasKey('shipping_addresses', $cartResponse); $shippingAddressResponse = current($cartResponse['shipping_addresses']); $this->assertNewAddressFields($billingAddressResponse); - $this->assertNewAddressFields($shippingAddressResponse); + $this->assertNewAddressFields($shippingAddressResponse, 'SHIPPING'); } /** @@ -244,9 +247,10 @@ public function testSetBillingAddressFromAddressBook() /** * Verify the all the whitelisted fields for a New Address Object * - * @param array $billingAddressResponse + * @param array $addressResponse + * @param string $addressType */ - private function assertNewAddressFields(array $billingAddressResponse): void + private function assertNewAddressFields(array $addressResponse, string $addressType = 'BILLING'): void { $assertionMap = [ ['response_field' => 'firstname', 'expected_value' => 'test firstname'], @@ -257,9 +261,10 @@ private function assertNewAddressFields(array $billingAddressResponse): void ['response_field' => 'postcode', 'expected_value' => '887766'], ['response_field' => 'telephone', 'expected_value' => '88776655'], ['response_field' => 'country', 'expected_value' => ['code' => 'US', 'label' => 'US']], + ['response_field' => 'address_type', 'expected_value' => $addressType] ]; - $this->assertResponseFields($billingAddressResponse, $assertionMap); + $this->assertResponseFields($addressResponse, $assertionMap); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php new file mode 100644 index 0000000000000..f159cb6f6151e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php @@ -0,0 +1,167 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting shipping methods on cart for guest + */ +class SetShippingMethodsOnCartTest extends GraphQlAbstract +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + } + + public function testShippingMethodWithVirtualProduct() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testShippingMethodWithSimpleProduct() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testShippingMethodWithSimpleProductWithoutAddress() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetShippingMethodWithMissedRequiredParameters() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetNonExistentShippingMethod() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetShippingMethodIfAddressIsNotBelongToCart() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetShippingMethodToNonExistentCart() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetShippingMethodToGuestCart() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetShippingMethodToAnotherCustomerCart() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetShippingMethodToNonExistentCartAddress() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetShippingMethodToGuestCartAddress() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetShippingMethodToAnotherCustomerCartAddress() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + public function testSetMultipleShippingMethods() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/422'); + } + + /** + * @param string $maskedQuoteId + * @param string $shippingMethodCode + * @param string $shippingCarrierCode + * @param string $shippingAddressId + * @return string + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + private function prepareMutationQuery( + string $maskedQuoteId, + string $shippingMethodCode, + string $shippingCarrierCode, + string $shippingAddressId + ) : string { + return <<<QUERY +mutation { + setShippingMethodsOnCart(input: + { + cart_id: "$maskedQuoteId", + shipping_addresses: [{ + cart_address_id: $shippingAddressId + shipping_method: { + method_code: "$shippingMethodCode" + carrier_code: "$shippingCarrierCode" + } + }] + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + label + amount + } + } + } + } +} + +QUERY; + } + + /** + * @param string $reversedQuoteId + * @return string + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + private function getMaskedQuoteIdByReversedQuoteId(string $reversedQuoteId): string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reversedQuoteId, 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingAddressOnCartTest.php index 1f9aca2f12013..4916bb2aa78c3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingAddressOnCartTest.php @@ -94,6 +94,7 @@ public function testSetNewShippingAddressByGuest() code label } + address_type } } } @@ -184,6 +185,7 @@ public function testSetNewShippingAddressByRegisteredCustomer() label code } + address_type } } } @@ -472,6 +474,7 @@ private function assertNewShippingAddressFields(array $shippingAddressResponse): ['response_field' => 'postcode', 'expected_value' => '887766'], ['response_field' => 'telephone', 'expected_value' => '88776655'], ['response_field' => 'country', 'expected_value' => ['code' => 'US', 'label' => 'US']], + ['response_field' => 'address_type', 'expected_value' => 'SHIPPING'] ]; $this->assertResponseFields($shippingAddressResponse, $assertionMap); diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml index 586ad2acee203..4995c1feb048e 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml @@ -75,7 +75,6 @@ <data name="salesRule/data/coupon_code" xsi:type="string">Lorem ipsum dolor sit amet, consectetur adipiscing elit - %isolation%</data> <data name="salesRule/data/simple_action" xsi:type="string">Fixed amount discount for whole cart</data> <data name="salesRule/data/discount_amount" xsi:type="string">60</data> - <data name="salesRule/data/apply_to_shipping" xsi:type="string">No</data> <data name="salesRule/data/simple_free_shipping" xsi:type="string">No</data> <data name="salesRule/data/store_labels/0" xsi:type="string">Coupon code+Fixed amount discount for whole cart</data> <data name="productForSalesRule1/dataset" xsi:type="string">simple_for_salesrule_1</data> diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/CategoryTest.php index 5721982ab8dc3..d4926e78040d6 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/CategoryTest.php @@ -38,8 +38,7 @@ public function testGetProductCollection() /** @var $collection \Magento\Catalog\Model\ResourceModel\Product\Collection */ $collection = $this->_model->getProductCollection(); $this->assertInstanceOf(\Magento\Catalog\Model\ResourceModel\Product\Collection::class, $collection); - $ids = $collection->getAllIds(); - $this->assertEquals(2, count($ids)); + $this->assertEquals(2, $collection->count()); $this->assertSame($collection, $this->_model->getProductCollection()); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_full_option_set.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_full_option_set.php index bd5dc541a15a5..5facba07d58f1 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_full_option_set.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_full_option_set.php @@ -187,6 +187,50 @@ 'price_type' => 'percent', 'sku' => 'sku2', 'max_characters' => 20, + ], + [ + 'title' => 'multiple option', + 'type' => 'multiple', + 'is_require' => true, + 'sort_order' => 7, + 'values' => [ + [ + 'title' => 'multiple option 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'multiple option 1 sku', + 'sort_order' => 1, + ], + [ + 'title' => 'multiple option 2', + 'price' => 20, + 'price_type' => 'fixed', + 'sku' => 'multiple option 2 sku', + 'sort_order' => 2, + ], + ], + ], + [ + 'title' => 'checkbox option', + 'type' => 'checkbox', + 'is_require' => true, + 'sort_order' => 6, + 'values' => [ + [ + 'title' => 'checkbox option 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'checkbox option 1 sku', + 'sort_order' => 1, + ], + [ + 'title' => 'checkbox option 2', + 'price' => 20, + 'price_type' => 'fixed', + 'sku' => 'checkbox option 2 sku', + 'sort_order' => 2, + ], + ], ] ]; diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/enable_all_shipping_methods.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/enable_all_shipping_methods.php new file mode 100644 index 0000000000000..dd48975aa2b09 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/enable_all_shipping_methods.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->create(WriterInterface::class); + +$configWriter->save('carriers/flatrate/active', 1); +$configWriter->save('carriers/tablerate/active', 1); +$configWriter->save('carriers/freeshipping/active', 1); +$configWriter->save('carriers/ups/active', 1); +$configWriter->save('carriers/usps/active', 1); +$configWriter->save('carriers/fedex/active', 1); +$configWriter->save('carriers/dhl/active', 1); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/enable_all_shipping_methods_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/enable_all_shipping_methods_rollback.php new file mode 100644 index 0000000000000..7a3ca79febf6d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/enable_all_shipping_methods_rollback.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->create(WriterInterface::class); + +$configWriter->delete('carriers/flatrate/active'); +$configWriter->delete('carriers/tablerate/active'); +$configWriter->delete('carriers/freeshipping/active'); +$configWriter->delete('carriers/ups/active'); +$configWriter->delete('carriers/usps/active'); +$configWriter->delete('carriers/fedex/active'); +$configWriter->delete('carriers/dhl/active'); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php index 234f0aae6a3ea..676433c0a1e6c 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php @@ -5,7 +5,19 @@ */ namespace Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier\Data; -class AssociatedProductsTest extends \PHPUnit\Framework\TestCase +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Filters\FilterModifier; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier\ConfigurablePanel; +use Magento\Framework\App\RequestInterface; +use PHPUnit\Framework\TestCase; + +/** + * AssociatedProductsTest + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class AssociatedProductsTest extends TestCase { /** * @var \Magento\Framework\ObjectManagerInterface $objectManager @@ -17,7 +29,10 @@ class AssociatedProductsTest extends \PHPUnit\Framework\TestCase */ private $registry; - public function setUp() + /** + * @inheritdoc + */ + public function setUp(): void { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $this->registry = $this->objectManager->get(\Magento\Framework\Registry::class); @@ -64,6 +79,53 @@ public function testGetProductMatrix($interfaceLocale) } } + /** + * Tests configurable product won't appear in product listing. + * + * Tests configurable product won't appear in configurable associated product listing if its options attribute + * is not filterable in grid. + * + * @return void + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoAppArea adminhtml + */ + public function testAddManuallyConfigurationsWithNotFilterableInGridAttribute(): void + { + /** @var RequestInterface $request */ + $request = $this->objectManager->get(RequestInterface::class); + $request->setParams([ + FilterModifier::FILTER_MODIFIER => [ + 'test_configurable' => [ + 'condition_type' => 'notnull', + ], + ], + 'attributes_codes' => [ + 'test_configurable' + ], + ]); + $context = $this->objectManager->create(ContextInterface::class, ['request' => $request]); + /** @var UiComponentFactory $uiComponentFactory */ + $uiComponentFactory = $this->objectManager->get(UiComponentFactory::class); + $uiComponent = $uiComponentFactory->create( + ConfigurablePanel::ASSOCIATED_PRODUCT_LISTING, + null, + ['context' => $context] + ); + + foreach ($uiComponent->getChildComponents() as $childUiComponent) { + $childUiComponent->prepare(); + } + + $dataSource = $uiComponent->getDataSourceData(); + $skus = array_column($dataSource['data']['items'], 'sku'); + + $this->assertNotContains( + 'configurable', + $skus, + 'Only products with specified attribute should be in product list' + ); + } + /** * @return array */ diff --git a/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_weight.php b/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_weight.php new file mode 100644 index 0000000000000..40e01a81ac807 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_weight.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$resource = $objectManager->get(\Magento\Framework\App\ResourceConnection::class); +$connection = $resource->getConnection(); +$resourceModel = $objectManager->create(\Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate::class); +$entityTable = $resourceModel->getTable('shipping_tablerate'); +$data = + [ + 'website_id' => 1, + 'dest_country_id' => 'US', + 'dest_region_id' => 0, + 'dest_zip' => '*', + 'condition_name' => 'package_weight', + 'condition_value' => 0, + 'price' => 10, + 'cost' => 0 + ]; +$connection->query( + "INSERT INTO {$entityTable} (`website_id`, `dest_country_id`, `dest_region_id`, `dest_zip`, `condition_name`," + . "`condition_value`, `price`, `cost`) VALUES (:website_id, :dest_country_id, :dest_region_id, :dest_zip," + . " :condition_name, :condition_value, :price, :cost);", + $data +); diff --git a/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_weight_rollback.php b/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_weight_rollback.php new file mode 100644 index 0000000000000..cb6e9353b8972 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_weight_rollback.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require 'tablerates_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php index cb83fa3abd857..3f7f8719fd587 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php @@ -72,7 +72,7 @@ protected function setUp() $this->api = $this->getMockBuilder(Nvp::class) ->disableOriginalConstructor() - ->setMethods(['call', 'getExportedShippingAddress', 'getExportedBillingAddress']) + ->setMethods(['call', 'getExportedShippingAddress', 'getExportedBillingAddress', 'getShippingRateCode']) ->getMock(); $this->api->expects($this->any()) @@ -302,6 +302,8 @@ public function testReturnFromPaypal() public function testReturnFromPaypalButton() { $quote = $this->getFixtureQuote(); + $quote->getShippingAddress()->setShippingMethod(''); + $this->prepareCheckoutModel($quote); $quote->getPayment()->setAdditionalInformation(Checkout::PAYMENT_INFO_BUTTON, 1); @@ -317,6 +319,8 @@ public function testReturnFromPaypalButton() $this->assertEquals($exportedShippingData['telephone'], $shippingAddress->getTelephone()); $this->assertEquals($exportedShippingData['email'], $shippingAddress->getEmail()); + $this->assertEquals('flatrate_flatrate', $shippingAddress->getShippingMethod()); + $this->assertEquals([$exportedShippingData['street']], $billingAddress->getStreet()); $this->assertEquals($exportedShippingData['firstname'], $billingAddress->getFirstname()); $this->assertEquals($exportedShippingData['city'], $billingAddress->getCity()); @@ -551,6 +555,9 @@ private function prepareCheckoutModel(Quote $quote, $prefix = '') $this->api->method('getExportedShippingAddress') ->willReturn($exportedShippingAddress); + $this->api->method('getShippingRateCode') + ->willReturn('flatrate_flatrate Flat Rate - Fixed'); + $this->paypalInfo->method('importToPayment') ->with($this->api, $quote->getPayment()); } @@ -573,7 +580,7 @@ private function getExportedData(): array 'city' => 'Denver', 'street' => '66 Pearl St', 'postcode' => '80203', - 'telephone' => '555-555-555' + 'telephone' => '555-555-555', ], 'billing' => [ 'email' => 'customer@example.com', diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/FormTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/FormTest.php new file mode 100644 index 0000000000000..1067a474e19aa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/FormTest.php @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Guest; + +use Magento\Customer\Model\Session; +use Magento\Framework\Data\Form\FormKey; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * @magentoAppIsolation enabled + */ +class FormTest extends AbstractController +{ + /** + * Test view order as guest with correct data + * + * @magentoDataFixture Magento/Sales/_files/order.php + */ + public function testViewOrderAsGuest() + { + $this->prepareRequestData(); + $this->dispatch('sales/guest/view/'); + $content = $this->getResponse()->getBody(); + $this->assertContains('Order # 100000001', $content); + } + + /** + * View order as logged in customer + * + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testViewOrderAsLoggedIn() + { + $this->login(1); + $this->dispatch('sales/guest/view/'); + $this->assertRedirect($this->stringContains('sales/order/history/')); + } + + /** + * Test attempting to open the Returns form as logged in customer + * + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testAttemptToOpenTheFormAsLoggedIn() + { + $this->login(1); + $this->dispatch('sales/guest/form/'); + $this->assertRedirect($this->stringContains('customer/account')); + } + + /** + * Test Return Order for guest with incorrect data + */ + public function testViewOrderAsGuestWithIncorrectData() + { + $this->prepareRequestData(true); + $this->dispatch('sales/guest/view/'); + $this->assertSessionMessages( + $this->equalTo(['You entered incorrect data. Please try again.']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * Login the user + * + * @param string $customerId Customer to mark as logged in for the session + * @return void + */ + protected function login($customerId) + { + /** @var Session $session */ + $session = $this->_objectManager->get(Session::class); + $session->loginById($customerId); + } + + /** + * @param bool $invalidData + * @return void + */ + private function prepareRequestData($invalidData = false) + { + $orderId = 100000001; + $email = $invalidData ? 'wrong@example.com' : 'customer@null.com'; + + /** @var FormKey $formKey */ + $formKey = $this->_objectManager->get(FormKey::class); + $post = [ + 'oar_order_id' => $orderId, + 'oar_billing_lastname' => 'lastname', + 'oar_type' => 'email', + 'oar_email' => $email, + 'oar_zip' => '', + 'form_key' => $formKey->getFormKey(), + ]; + + $this->getRequest()->setMethod(Request::METHOD_POST); + $this->getRequest()->setPostValue($post); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/IndexTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/IndexTest.php index 92eae7a3fe3d7..940d05eb4d5d7 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/IndexTest.php @@ -128,6 +128,7 @@ public function testAddActionProductNameXss() public function testAllcartAction() { $formKey = $this->_objectManager->get(\Magento\Framework\Data\Form\FormKey::class)->getFormKey(); + $this->getRequest()->setMethod('POST'); $this->getRequest()->setParam('form_key', $formKey); $this->dispatch('wishlist/index/allcart'); diff --git a/dev/tests/static/framework/Magento/ruleset.xml b/dev/tests/static/framework/Magento/ruleset.xml index d0f0d38f8fa03..9dde4b7bd7d32 100644 --- a/dev/tests/static/framework/Magento/ruleset.xml +++ b/dev/tests/static/framework/Magento/ruleset.xml @@ -84,5 +84,6 @@ <rule ref="Squiz.Commenting.DocCommentAlignment"/> <rule ref="Squiz.Functions.GlobalFunction"/> + <rule ref="Squiz.Operators.ValidLogicalOperators"/> <rule ref="Squiz.WhiteSpace.LogicalOperatorSpacing"/> </ruleset> diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Search/RequestConfigTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Search/RequestConfigTest.php index 4c0fed148aea8..f0177c449a3f9 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Search/RequestConfigTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Search/RequestConfigTest.php @@ -102,8 +102,7 @@ public function testFileSchemaUsingInvalidXml($expectedErrors = null) Element 'metric', attribute 'type': [facet 'enumeration'] " . "The value 'sumasdasd' is not an element of the set {'sum', 'count', 'min', 'max', 'avg'}. Element 'metric', attribute 'type': 'sumasdasd' is not a valid value of the local atomic type. -Element 'bucket': Missing child element(s). Expected is one of ( metrics, ranges ). -Element 'request': Missing child element(s). Expected is ( from )." +Element 'bucket': Missing child element(s). Expected is one of ( metrics, ranges )." ) ); parent::testFileSchemaUsingInvalidXml($expectedErrors); diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt index 0450ae1330cd8..c8f363dfbb716 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt +++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt @@ -209,3 +209,8 @@ Magento/Customer/Model/FileUploaderDataResolver.php Magento/Customer/Model/Customer/DataProvider.php Magento/InventoryShippingAdminUi/Ui/DataProvider Magento/Elasticsearch6/Model/Client +Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection +Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection +Magento/Elasticsearch/Model/Layer/Category/ItemCollectionProvider +Magento/Elasticsearch/Model/Layer/Search/ItemCollectionProvider +Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/CompositeResolver diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php index 897b67d8d46ec..f4d83ece134cf 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php @@ -86,6 +86,7 @@ public function query(RequestInterface $request) $response = [ 'documents' => $documents, 'aggregations' => $aggregations, + 'total' => count($documents) ]; return $this->responseFactory->create($response); } diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/ResponseFactory.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/ResponseFactory.php index 776dab93c2dcd..b41a43883fde5 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/ResponseFactory.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/ResponseFactory.php @@ -68,7 +68,8 @@ public function create($rawResponse) \Magento\Framework\Search\Response\QueryResponse::class, [ 'documents' => $documents, - 'aggregations' => $aggregations + 'aggregations' => $aggregations, + 'total' => $rawResponse['total'] ] ); } diff --git a/lib/internal/Magento/Framework/Search/Request.php b/lib/internal/Magento/Framework/Search/Request.php index d55b2085e87ef..60f3338046613 100644 --- a/lib/internal/Magento/Framework/Search/Request.php +++ b/lib/internal/Magento/Framework/Search/Request.php @@ -54,6 +54,11 @@ class Request implements RequestInterface */ protected $dimensions; + /** + * @var array + */ + private $sort; + /** * @param string $name * @param string $indexName @@ -62,6 +67,7 @@ class Request implements RequestInterface * @param int|null $size * @param Dimension[] $dimensions * @param RequestBucketInterface[] $buckets + * @param array $sort */ public function __construct( $name, @@ -70,7 +76,8 @@ public function __construct( $from = null, $size = null, array $dimensions = [], - array $buckets = [] + array $buckets = [], + $sort = [] ) { $this->name = $name; $this->index = $indexName; @@ -79,10 +86,11 @@ public function __construct( $this->size = $size; $this->buckets = $buckets; $this->dimensions = $dimensions; + $this->sort = $sort; } /** - * {@inheritdoc} + * @inheritdoc */ public function getName() { @@ -90,7 +98,7 @@ public function getName() } /** - * {@inheritdoc} + * @inheritdoc */ public function getIndex() { @@ -98,7 +106,7 @@ public function getIndex() } /** - * {@inheritdoc} + * @inheritdoc */ public function getDimensions() { @@ -106,7 +114,7 @@ public function getDimensions() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAggregation() { @@ -114,7 +122,7 @@ public function getAggregation() } /** - * {@inheritdoc} + * @inheritdoc */ public function getQuery() { @@ -122,7 +130,7 @@ public function getQuery() } /** - * {@inheritdoc} + * @inheritdoc */ public function getFrom() { @@ -130,10 +138,18 @@ public function getFrom() } /** - * {@inheritdoc} + * @inheritdoc */ public function getSize() { return $this->size; } + + /** + * @inheritdoc + */ + public function getSort() + { + return $this->sort; + } } diff --git a/lib/internal/Magento/Framework/Search/Request/Binder.php b/lib/internal/Magento/Framework/Search/Request/Binder.php index 34b5d72005e74..9df1ee87eb869 100644 --- a/lib/internal/Magento/Framework/Search/Request/Binder.php +++ b/lib/internal/Magento/Framework/Search/Request/Binder.php @@ -6,6 +6,8 @@ namespace Magento\Framework\Search\Request; /** + * Data binder for search request. + * * @api */ class Binder @@ -24,6 +26,9 @@ public function bind(array $requestData, array $bindData) $data['queries'] = $this->processData($requestData['queries'], $bindData['placeholder']); $data['filters'] = $this->processData($requestData['filters'], $bindData['placeholder']); $data['aggregations'] = $this->processData($requestData['aggregations'], $bindData['placeholder']); + if (isset($bindData['sort']) && isset($requestData['sort'])) { + $data['sort'] = $this->processData($requestData['sort'], $bindData['sort']); + } return $data; } @@ -48,6 +53,8 @@ private function processLimits($data, $bindData) } /** + * Dimensions process. + * * @param array $data * @param array $bindData * @return array diff --git a/lib/internal/Magento/Framework/Search/Request/Builder.php b/lib/internal/Magento/Framework/Search/Request/Builder.php index a16b3369cdda0..74bc65010a934 100644 --- a/lib/internal/Magento/Framework/Search/Request/Builder.php +++ b/lib/internal/Magento/Framework/Search/Request/Builder.php @@ -11,7 +11,10 @@ use Magento\Framework\Search\RequestInterface; /** + * Search request builder. + * * @api + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Builder { @@ -95,6 +98,18 @@ public function setFrom($from) return $this; } + /** + * Set sort. + * + * @param \Magento\Framework\Api\SortOrder[] $sort + * @return $this + */ + public function setSort($sort) + { + $this->data['sort'] = $sort; + return $this; + } + /** * Bind dimension data by name * @@ -139,6 +154,9 @@ public function create() } $data = $this->binder->bind($data, $this->data); + if (isset($this->data['sort'])) { + $data['sort'] = $this->prepareSorts($this->data['sort']); + } $data = $this->cleaner->clean($data); $this->clear(); @@ -146,6 +164,25 @@ public function create() return $this->convert($data); } + /** + * Prepare sort data for request. + * + * @param array $sorts + * @return array + */ + private function prepareSorts(array $sorts) + { + $sortData = []; + foreach ($sorts as $sortField => $direction) { + $sortData[] = [ + 'field' => $sortField, + 'direction' => $direction, + ]; + } + + return $sortData; + } + /** * Clear data * @@ -178,21 +215,27 @@ private function convert($data) 'filters' => $data['filters'] ] ); + $requestData = [ + 'name' => $data['query'], + 'indexName' => $data['index'], + 'from' => $data['from'], + 'size' => $data['size'], + 'query' => $mapper->getRootQuery(), + 'dimensions' => $this->buildDimensions(isset($data['dimensions']) ? $data['dimensions'] : []), + 'buckets' => $mapper->getBuckets() + ]; + if (isset($data['sort'])) { + $requestData['sort'] = $data['sort']; + } return $this->objectManager->create( \Magento\Framework\Search\Request::class, - [ - 'name' => $data['query'], - 'indexName' => $data['index'], - 'from' => $data['from'], - 'size' => $data['size'], - 'query' => $mapper->getRootQuery(), - 'dimensions' => $this->buildDimensions(isset($data['dimensions']) ? $data['dimensions'] : []), - 'buckets' => $mapper->getBuckets() - ] + $requestData ); } /** + * Build dimensions. + * * @param array $dimensionsData * @return array */ diff --git a/lib/internal/Magento/Framework/Search/RequestInterface.php b/lib/internal/Magento/Framework/Search/RequestInterface.php index 16df80f755c07..2de756e754a27 100644 --- a/lib/internal/Magento/Framework/Search/RequestInterface.php +++ b/lib/internal/Magento/Framework/Search/RequestInterface.php @@ -64,4 +64,11 @@ public function getFrom(); * @return int|null */ public function getSize(); + + /** + * Get Sort items + * + * @return array + */ + public function getSort(); } diff --git a/lib/internal/Magento/Framework/Search/Response/QueryResponse.php b/lib/internal/Magento/Framework/Search/Response/QueryResponse.php index c2328398e53dc..90c7056ea2549 100644 --- a/lib/internal/Magento/Framework/Search/Response/QueryResponse.php +++ b/lib/internal/Magento/Framework/Search/Response/QueryResponse.php @@ -29,18 +29,26 @@ class QueryResponse implements ResponseInterface */ protected $aggregations; + /** + * @var int + */ + private $total; + /** * @param Document[] $documents * @param AggregationInterface $aggregations + * @param int $total */ - public function __construct(array $documents, AggregationInterface $aggregations) + public function __construct(array $documents, AggregationInterface $aggregations, int $total = 0) { $this->documents = $documents; $this->aggregations = $aggregations; + $this->total = $total; } /** - * Countable: return count of fields in document + * Countable: return count of fields in document. + * * @return int */ public function count() @@ -59,10 +67,18 @@ public function getIterator() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAggregations() { return $this->aggregations; } + + /** + * @inheritdoc + */ + public function getTotal(): int + { + return $this->total; + } } diff --git a/lib/internal/Magento/Framework/Search/ResponseInterface.php b/lib/internal/Magento/Framework/Search/ResponseInterface.php index 3b89528532602..c6c0d0ab59e10 100644 --- a/lib/internal/Magento/Framework/Search/ResponseInterface.php +++ b/lib/internal/Magento/Framework/Search/ResponseInterface.php @@ -16,4 +16,11 @@ interface ResponseInterface extends \IteratorAggregate, \Countable * @return \Magento\Framework\Api\Search\AggregationInterface */ public function getAggregations(); + + /** + * Return total count of items. + * + * @return int + */ + public function getTotal(): int; } diff --git a/lib/internal/Magento/Framework/Search/Search.php b/lib/internal/Magento/Framework/Search/Search.php index 16dfc6fdd1ddb..fe228546b55fb 100644 --- a/lib/internal/Magento/Framework/Search/Search.php +++ b/lib/internal/Magento/Framework/Search/Search.php @@ -10,6 +10,9 @@ use Magento\Framework\App\ScopeResolverInterface; use Magento\Framework\Search\Request\Builder; +/** + * Search API for all requests. + */ class Search implements SearchInterface { /** @@ -51,7 +54,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function search(SearchCriteriaInterface $searchCriteria) { @@ -68,6 +71,7 @@ public function search(SearchCriteriaInterface $searchCriteria) $this->requestBuilder->setFrom($searchCriteria->getCurrentPage() * $searchCriteria->getPageSize()); $this->requestBuilder->setSize($searchCriteria->getPageSize()); + $this->requestBuilder->setSort($searchCriteria->getSortOrders()); $request = $this->requestBuilder->create(); $searchResponse = $this->searchEngine->search($request); diff --git a/lib/internal/Magento/Framework/Search/SearchResponseBuilder.php b/lib/internal/Magento/Framework/Search/SearchResponseBuilder.php index 40fcd493861d7..2314252f4609c 100644 --- a/lib/internal/Magento/Framework/Search/SearchResponseBuilder.php +++ b/lib/internal/Magento/Framework/Search/SearchResponseBuilder.php @@ -9,6 +9,9 @@ use Magento\Framework\Api\Search\DocumentFactory; use Magento\Framework\Api\Search\SearchResultFactory; +/** + * Builder for search response. + */ class SearchResponseBuilder { /** @@ -35,6 +38,8 @@ public function __construct( } /** + * Build search result by search response. + * * @param ResponseInterface $response * @return SearchResultInterface */ @@ -46,7 +51,7 @@ public function build(ResponseInterface $response) $documents = iterator_to_array($response); $searchResult->setItems($documents); $searchResult->setAggregations($response->getAggregations()); - $searchResult->setTotalCount(count($documents)); + $searchResult->setTotalCount($response->getTotal()); return $searchResult; } diff --git a/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/AdapterTest.php b/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/AdapterTest.php index 55d26493ca379..a35e1fd8b6151 100644 --- a/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/AdapterTest.php +++ b/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/AdapterTest.php @@ -155,6 +155,7 @@ public function testQuery() 'aggregation2' => [2, 4], ], ], + 'total' => 1 ]; $select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) diff --git a/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/ResponseFactoryTest.php b/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/ResponseFactoryTest.php index c0699156decd0..3d21064b13d47 100644 --- a/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/ResponseFactoryTest.php +++ b/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/ResponseFactoryTest.php @@ -49,6 +49,7 @@ public function testCreate() ['title' => 'twoTitle', 'description' => 'twoDescription'], ], 'aggregations' => [], + 'total' => 2 ]; $this->documentFactory->expects($this->at(0))->method('create') @@ -61,7 +62,7 @@ public function testCreate() $this->objectManager->expects($this->once())->method('create') ->with( $this->equalTo(\Magento\Framework\Search\Response\QueryResponse::class), - $this->equalTo(['documents' => ['document1', 'document2'], 'aggregations' => null]) + $this->equalTo(['documents' => ['document1', 'document2'], 'aggregations' => null, 'total' => 2]) ) ->will($this->returnValue('QueryResponseObject')); diff --git a/lib/internal/Magento/Framework/Search/Test/Unit/Response/QueryResponseTest.php b/lib/internal/Magento/Framework/Search/Test/Unit/Response/QueryResponseTest.php index a60a280cb602a..a994559f28f0b 100644 --- a/lib/internal/Magento/Framework/Search/Test/Unit/Response/QueryResponseTest.php +++ b/lib/internal/Magento/Framework/Search/Test/Unit/Response/QueryResponseTest.php @@ -46,6 +46,7 @@ protected function setUp() [ 'documents' => $this->documents, 'aggregations' => $this->aggregations, + 'total' => 1 ] ); } diff --git a/lib/internal/Magento/Framework/Search/Test/Unit/SearchResponseBuilderTest.php b/lib/internal/Magento/Framework/Search/Test/Unit/SearchResponseBuilderTest.php index a0b11959a42db..9ffafae9622f3 100644 --- a/lib/internal/Magento/Framework/Search/Test/Unit/SearchResponseBuilderTest.php +++ b/lib/internal/Magento/Framework/Search/Test/Unit/SearchResponseBuilderTest.php @@ -67,7 +67,7 @@ public function testBuild() /** @var QueryResponse|\PHPUnit_Framework_MockObject_MockObject $response */ $response = $this->getMockBuilder(\Magento\Framework\Search\Response\QueryResponse::class) - ->setMethods(['getIterator', 'getAggregations']) + ->setMethods(['getIterator', 'getAggregations', 'getTotal']) ->disableOriginalConstructor() ->getMockForAbstractClass(); $response->expects($this->any()) @@ -76,6 +76,9 @@ public function testBuild() $response->expects($this->once()) ->method('getAggregations') ->willReturn($aggregations); + $response->expects($this->any()) + ->method('getTotal') + ->willReturn(1); $result = $this->model->build($response); diff --git a/lib/internal/Magento/Framework/Search/etc/requests.xsd b/lib/internal/Magento/Framework/Search/etc/requests.xsd index a49124748b295..7d277382b698f 100644 --- a/lib/internal/Magento/Framework/Search/etc/requests.xsd +++ b/lib/internal/Magento/Framework/Search/etc/requests.xsd @@ -28,8 +28,8 @@ <xs:field xpath="@name" /> </xs:key> </xs:element> - <xs:element type="xs:string" name="from" /> - <xs:element type="xs:string" name="size" /> + <xs:element type="xs:string" name="from" minOccurs="0" /> + <xs:element type="xs:string" name="size" minOccurs="0" /> </xs:sequence> <xs:attribute type="xs:string" name="query" use="required" /> <xs:attribute type="xs:string" name="index" use="required" /> diff --git a/lib/web/mage/tabs.js b/lib/web/mage/tabs.js index b441477ab8d8a..65c452d33bf12 100644 --- a/lib/web/mage/tabs.js +++ b/lib/web/mage/tabs.js @@ -72,7 +72,7 @@ define([ if (anchor && isValid) { $.each(self.contents, function (i) { - if ($(this).attr('id') === anchorId) { + if ($(this).attr('id') === anchorId || $(this).find('#' + anchorId).length) { self.collapsibles.not(self.collapsibles.eq(i)).collapsible('forceDeactivate'); return false;