Collection.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. declare(strict_types=1);
  7. namespace Magento\Quote\Model\ResourceModel\Quote\Item;
  8. use Magento\Catalog\Api\Data\ProductInterface;
  9. use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection;
  10. use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus;
  11. use Magento\Quote\Model\Quote;
  12. use Magento\Quote\Model\Quote\Item as QuoteItem;
  13. use Magento\Quote\Model\ResourceModel\Quote\Item as ResourceQuoteItem;
  14. /**
  15. * Quote item resource collection
  16. *
  17. * @api
  18. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  19. * @since 100.0.2
  20. */
  21. class Collection extends \Magento\Framework\Model\ResourceModel\Db\VersionControl\Collection
  22. {
  23. /**
  24. * Collection quote instance
  25. *
  26. * @var \Magento\Quote\Model\Quote
  27. */
  28. protected $_quote;
  29. /**
  30. * Product Ids array
  31. *
  32. * @var int[]
  33. */
  34. protected $_productIds = [];
  35. /**
  36. * @var \Magento\Quote\Model\ResourceModel\Quote\Item\Option\CollectionFactory
  37. */
  38. protected $_itemOptionCollectionFactory;
  39. /**
  40. * @var \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory
  41. */
  42. protected $_productCollectionFactory;
  43. /**
  44. * @var \Magento\Quote\Model\Quote\Config
  45. */
  46. protected $_quoteConfig;
  47. /**
  48. * @var \Magento\Store\Model\StoreManagerInterface|null
  49. */
  50. private $storeManager;
  51. /**
  52. * @var bool $recollectQuote
  53. */
  54. private $recollectQuote = false;
  55. /**
  56. * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory
  57. * @param \Psr\Log\LoggerInterface $logger
  58. * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy
  59. * @param \Magento\Framework\Event\ManagerInterface $eventManager
  60. * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot
  61. * @param Option\CollectionFactory $itemOptionCollectionFactory
  62. * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory
  63. * @param \Magento\Quote\Model\Quote\Config $quoteConfig
  64. * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection
  65. * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource
  66. * @param \Magento\Store\Model\StoreManagerInterface|null $storeManager
  67. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  68. */
  69. public function __construct(
  70. \Magento\Framework\Data\Collection\EntityFactory $entityFactory,
  71. \Psr\Log\LoggerInterface $logger,
  72. \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy,
  73. \Magento\Framework\Event\ManagerInterface $eventManager,
  74. \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot,
  75. \Magento\Quote\Model\ResourceModel\Quote\Item\Option\CollectionFactory $itemOptionCollectionFactory,
  76. \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory,
  77. \Magento\Quote\Model\Quote\Config $quoteConfig,
  78. \Magento\Framework\DB\Adapter\AdapterInterface $connection = null,
  79. \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null,
  80. \Magento\Store\Model\StoreManagerInterface $storeManager = null
  81. ) {
  82. parent::__construct(
  83. $entityFactory,
  84. $logger,
  85. $fetchStrategy,
  86. $eventManager,
  87. $entitySnapshot,
  88. $connection,
  89. $resource
  90. );
  91. $this->_itemOptionCollectionFactory = $itemOptionCollectionFactory;
  92. $this->_productCollectionFactory = $productCollectionFactory;
  93. $this->_quoteConfig = $quoteConfig;
  94. // Backward compatibility constructor parameters
  95. $this->storeManager = $storeManager ?:
  96. \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Store\Model\StoreManagerInterface::class);
  97. }
  98. /**
  99. * Initialize resource model
  100. *
  101. * @return void
  102. */
  103. protected function _construct()
  104. {
  105. $this->_init(QuoteItem::class, ResourceQuoteItem::class);
  106. }
  107. /**
  108. * Retrieve store Id (From Quote)
  109. *
  110. * @return int
  111. */
  112. public function getStoreId(): int
  113. {
  114. // Fallback to current storeId if no quote is provided
  115. // (see https://github.com/magento/magento2/commit/9d3be732a88884a66d667b443b3dc1655ddd0721)
  116. return $this->_quote === null ?
  117. (int) $this->storeManager->getStore()->getId() : (int) $this->_quote->getStoreId();
  118. }
  119. /**
  120. * Set Quote object to Collection.
  121. *
  122. * @param Quote $quote
  123. * @return $this
  124. */
  125. public function setQuote($quote): self
  126. {
  127. $this->_quote = $quote;
  128. $quoteId = $quote->getId();
  129. if ($quoteId) {
  130. $this->addFieldToFilter('quote_id', $quote->getId());
  131. } else {
  132. $this->_totalRecords = 0;
  133. $this->_setIsLoaded(true);
  134. }
  135. return $this;
  136. }
  137. /**
  138. * Reset the collection and inner join it to quotes table.
  139. *
  140. * Optionally can select items with specified product id only
  141. *
  142. * @param string $quotesTableName
  143. * @param int $productId
  144. * @return $this
  145. */
  146. public function resetJoinQuotes($quotesTableName, $productId = null): self
  147. {
  148. $this->getSelect()->reset()->from(
  149. ['qi' => $this->getResource()->getMainTable()],
  150. ['item_id', 'qty', 'quote_id']
  151. )->joinInner(
  152. ['q' => $quotesTableName],
  153. 'qi.quote_id = q.entity_id',
  154. ['store_id', 'items_qty', 'items_count']
  155. );
  156. if ($productId) {
  157. $this->getSelect()->where('qi.product_id = ?', (int)$productId);
  158. }
  159. return $this;
  160. }
  161. /**
  162. * After load processing.
  163. *
  164. * @return $this
  165. */
  166. protected function _afterLoad(): self
  167. {
  168. parent::_afterLoad();
  169. $productIds = [];
  170. foreach ($this as $item) {
  171. // Assign parent items
  172. if ($item->getParentItemId()) {
  173. $item->setParentItem($this->getItemById($item->getParentItemId()));
  174. }
  175. if ($this->_quote) {
  176. $item->setQuote($this->_quote);
  177. }
  178. // Collect quote products ids
  179. $productIds[] = (int)$item->getProductId();
  180. }
  181. $this->_productIds = array_merge($this->_productIds, $productIds);
  182. $this->removeItemsWithAbsentProducts();
  183. /**
  184. * Assign options and products
  185. */
  186. $this->_assignOptions();
  187. $this->_assignProducts();
  188. $this->resetItemsDataChanged();
  189. return $this;
  190. }
  191. /**
  192. * Add options to items.
  193. *
  194. * @return $this
  195. */
  196. protected function _assignOptions(): self
  197. {
  198. $itemIds = array_keys($this->_items);
  199. $optionCollection = $this->_itemOptionCollectionFactory->create()->addItemFilter($itemIds);
  200. foreach ($this as $item) {
  201. $item->setOptions($optionCollection->getOptionsByItem($item));
  202. }
  203. $productIds = $optionCollection->getProductIds();
  204. $this->_productIds = array_merge($this->_productIds, $productIds);
  205. return $this;
  206. }
  207. /**
  208. * Add products to items and item options.
  209. *
  210. * @return $this
  211. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  212. */
  213. protected function _assignProducts(): self
  214. {
  215. \Magento\Framework\Profiler::start('QUOTE:' . __METHOD__, ['group' => 'QUOTE', 'method' => __METHOD__]);
  216. $productCollection = $this->_productCollectionFactory->create()->setStoreId(
  217. $this->getStoreId()
  218. )->addIdFilter(
  219. $this->_productIds
  220. )->addAttributeToSelect(
  221. $this->_quoteConfig->getProductAttributes()
  222. );
  223. $this->skipStockStatusFilter($productCollection);
  224. $productCollection->addOptionsToResult()->addStoreFilter()->addUrlRewrite();
  225. $this->_eventManager->dispatch(
  226. 'prepare_catalog_product_collection_prices',
  227. ['collection' => $productCollection, 'store_id' => $this->getStoreId()]
  228. );
  229. $this->_eventManager->dispatch(
  230. 'sales_quote_item_collection_products_after_load',
  231. ['collection' => $productCollection]
  232. );
  233. foreach ($this as $item) {
  234. /** @var ProductInterface $product */
  235. $product = $productCollection->getItemById($item->getProductId());
  236. $qtyOptions = [];
  237. if ($product && $this->isValidProduct($product)) {
  238. $product->setCustomOptions([]);
  239. $optionProductIds = $this->getOptionProductIds($item, $product, $productCollection);
  240. foreach ($optionProductIds as $optionProductId) {
  241. $qtyOption = $item->getOptionByCode('product_qty_' . $optionProductId);
  242. if ($qtyOption) {
  243. $qtyOptions[$optionProductId] = $qtyOption;
  244. }
  245. }
  246. } else {
  247. $item->isDeleted(true);
  248. $this->recollectQuote = true;
  249. }
  250. if (!$item->isDeleted()) {
  251. $item->setQtyOptions($qtyOptions)->setProduct($product);
  252. $item->checkData();
  253. }
  254. }
  255. if ($this->recollectQuote && $this->_quote) {
  256. $this->_quote->collectTotals();
  257. }
  258. \Magento\Framework\Profiler::stop('QUOTE:' . __METHOD__);
  259. return $this;
  260. }
  261. /**
  262. * Get product Ids from option.
  263. *
  264. * @param QuoteItem $item
  265. * @param ProductInterface $product
  266. * @param ProductCollection $productCollection
  267. * @return array
  268. */
  269. private function getOptionProductIds(
  270. QuoteItem $item,
  271. ProductInterface $product,
  272. ProductCollection $productCollection
  273. ): array {
  274. $optionProductIds = [];
  275. foreach ($item->getOptions() as $option) {
  276. /**
  277. * Call type-specific logic for product associated with quote item
  278. */
  279. $product->getTypeInstance()->assignProductToOption(
  280. $productCollection->getItemById($option->getProductId()),
  281. $option,
  282. $product
  283. );
  284. if (is_object($option->getProduct()) && $option->getProduct()->getId() != $product->getId()) {
  285. $isValidProduct = $this->isValidProduct($option->getProduct());
  286. if (!$isValidProduct && !$item->isDeleted()) {
  287. $item->isDeleted(true);
  288. $this->recollectQuote = true;
  289. continue;
  290. }
  291. $optionProductIds[$option->getProduct()->getId()] = $option->getProduct()->getId();
  292. }
  293. }
  294. return $optionProductIds;
  295. }
  296. /**
  297. * Check is valid product.
  298. *
  299. * @param ProductInterface $product
  300. * @return bool
  301. */
  302. private function isValidProduct(ProductInterface $product): bool
  303. {
  304. $result = ($product && (int)$product->getStatus() !== ProductStatus::STATUS_DISABLED);
  305. return $result;
  306. }
  307. /**
  308. * Prevents adding stock status filter to the collection of products.
  309. *
  310. * @param ProductCollection $productCollection
  311. * @return void
  312. *
  313. * @see \Magento\CatalogInventory\Helper\Stock::addIsInStockFilterToCollection
  314. */
  315. private function skipStockStatusFilter(ProductCollection $productCollection): void
  316. {
  317. $productCollection->setFlag('has_stock_status_filter', true);
  318. }
  319. /**
  320. * Find and remove quote items with non existing products
  321. *
  322. * @return void
  323. */
  324. private function removeItemsWithAbsentProducts(): void
  325. {
  326. if (count($this->_productIds) === 0) {
  327. return;
  328. }
  329. $productCollection = $this->_productCollectionFactory->create()->addIdFilter($this->_productIds);
  330. $existingProductsIds = $productCollection->getAllIds();
  331. $absentProductsIds = array_diff($this->_productIds, $existingProductsIds);
  332. // Remove not existing products from items collection
  333. if (!empty($absentProductsIds)) {
  334. foreach ($absentProductsIds as $productIdToExclude) {
  335. /** @var \Magento\Quote\Model\Quote\Item $quoteItem */
  336. $quoteItem = $this->getItemByColumnValue('product_id', $productIdToExclude);
  337. $this->removeItemByKey($quoteItem->getId());
  338. }
  339. }
  340. }
  341. }