_itemOptionCollectionFactory = $itemOptionCollectionFactory; $this->_productCollectionFactory = $productCollectionFactory; $this->_quoteConfig = $quoteConfig; // Backward compatibility constructor parameters $this->storeManager = $storeManager ?: \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Store\Model\StoreManagerInterface::class); } /** * Initialize resource model * * @return void */ protected function _construct() { $this->_init(QuoteItem::class, ResourceQuoteItem::class); } /** * Retrieve store Id (From Quote) * * @return int */ public function getStoreId(): int { // Fallback to current storeId if no quote is provided // (see https://github.com/magento/magento2/commit/9d3be732a88884a66d667b443b3dc1655ddd0721) return $this->_quote === null ? (int) $this->storeManager->getStore()->getId() : (int) $this->_quote->getStoreId(); } /** * Set Quote object to Collection. * * @param Quote $quote * @return $this */ public function setQuote($quote): self { $this->_quote = $quote; $quoteId = $quote->getId(); if ($quoteId) { $this->addFieldToFilter('quote_id', $quote->getId()); } else { $this->_totalRecords = 0; $this->_setIsLoaded(true); } return $this; } /** * Reset the collection and inner join it to quotes table. * * Optionally can select items with specified product id only * * @param string $quotesTableName * @param int $productId * @return $this */ public function resetJoinQuotes($quotesTableName, $productId = null): self { $this->getSelect()->reset()->from( ['qi' => $this->getResource()->getMainTable()], ['item_id', 'qty', 'quote_id'] )->joinInner( ['q' => $quotesTableName], 'qi.quote_id = q.entity_id', ['store_id', 'items_qty', 'items_count'] ); if ($productId) { $this->getSelect()->where('qi.product_id = ?', (int)$productId); } return $this; } /** * After load processing. * * @return $this */ protected function _afterLoad(): self { parent::_afterLoad(); $productIds = []; foreach ($this as $item) { // Assign parent items if ($item->getParentItemId()) { $item->setParentItem($this->getItemById($item->getParentItemId())); } if ($this->_quote) { $item->setQuote($this->_quote); } // Collect quote products ids $productIds[] = (int)$item->getProductId(); } $this->_productIds = array_merge($this->_productIds, $productIds); $this->removeItemsWithAbsentProducts(); /** * Assign options and products */ $this->_assignOptions(); $this->_assignProducts(); $this->resetItemsDataChanged(); return $this; } /** * Add options to items. * * @return $this */ protected function _assignOptions(): self { $itemIds = array_keys($this->_items); $optionCollection = $this->_itemOptionCollectionFactory->create()->addItemFilter($itemIds); foreach ($this as $item) { $item->setOptions($optionCollection->getOptionsByItem($item)); } $productIds = $optionCollection->getProductIds(); $this->_productIds = array_merge($this->_productIds, $productIds); return $this; } /** * Add products to items and item options. * * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function _assignProducts(): self { \Magento\Framework\Profiler::start('QUOTE:' . __METHOD__, ['group' => 'QUOTE', 'method' => __METHOD__]); $productCollection = $this->_productCollectionFactory->create()->setStoreId( $this->getStoreId() )->addIdFilter( $this->_productIds )->addAttributeToSelect( $this->_quoteConfig->getProductAttributes() ); $this->skipStockStatusFilter($productCollection); $productCollection->addOptionsToResult()->addStoreFilter()->addUrlRewrite(); $this->_eventManager->dispatch( 'prepare_catalog_product_collection_prices', ['collection' => $productCollection, 'store_id' => $this->getStoreId()] ); $this->_eventManager->dispatch( 'sales_quote_item_collection_products_after_load', ['collection' => $productCollection] ); foreach ($this as $item) { /** @var ProductInterface $product */ $product = $productCollection->getItemById($item->getProductId()); $qtyOptions = []; if ($product && $this->isValidProduct($product)) { $product->setCustomOptions([]); $optionProductIds = $this->getOptionProductIds($item, $product, $productCollection); foreach ($optionProductIds as $optionProductId) { $qtyOption = $item->getOptionByCode('product_qty_' . $optionProductId); if ($qtyOption) { $qtyOptions[$optionProductId] = $qtyOption; } } } else { $item->isDeleted(true); $this->recollectQuote = true; } if (!$item->isDeleted()) { $item->setQtyOptions($qtyOptions)->setProduct($product); $item->checkData(); } } if ($this->recollectQuote && $this->_quote) { $this->_quote->collectTotals(); } \Magento\Framework\Profiler::stop('QUOTE:' . __METHOD__); return $this; } /** * Get product Ids from option. * * @param QuoteItem $item * @param ProductInterface $product * @param ProductCollection $productCollection * @return array */ private function getOptionProductIds( QuoteItem $item, ProductInterface $product, ProductCollection $productCollection ): array { $optionProductIds = []; foreach ($item->getOptions() as $option) { /** * Call type-specific logic for product associated with quote item */ $product->getTypeInstance()->assignProductToOption( $productCollection->getItemById($option->getProductId()), $option, $product ); if (is_object($option->getProduct()) && $option->getProduct()->getId() != $product->getId()) { $isValidProduct = $this->isValidProduct($option->getProduct()); if (!$isValidProduct && !$item->isDeleted()) { $item->isDeleted(true); $this->recollectQuote = true; continue; } $optionProductIds[$option->getProduct()->getId()] = $option->getProduct()->getId(); } } return $optionProductIds; } /** * Check is valid product. * * @param ProductInterface $product * @return bool */ private function isValidProduct(ProductInterface $product): bool { $result = ($product && (int)$product->getStatus() !== ProductStatus::STATUS_DISABLED); return $result; } /** * Prevents adding stock status filter to the collection of products. * * @param ProductCollection $productCollection * @return void * * @see \Magento\CatalogInventory\Helper\Stock::addIsInStockFilterToCollection */ private function skipStockStatusFilter(ProductCollection $productCollection): void { $productCollection->setFlag('has_stock_status_filter', true); } /** * Find and remove quote items with non existing products * * @return void */ private function removeItemsWithAbsentProducts(): void { if (count($this->_productIds) === 0) { return; } $productCollection = $this->_productCollectionFactory->create()->addIdFilter($this->_productIds); $existingProductsIds = $productCollection->getAllIds(); $absentProductsIds = array_diff($this->_productIds, $existingProductsIds); // Remove not existing products from items collection if (!empty($absentProductsIds)) { foreach ($absentProductsIds as $productIdToExclude) { /** @var \Magento\Quote\Model\Quote\Item $quoteItem */ $quoteItem = $this->getItemByColumnValue('product_id', $productIdToExclude); $this->removeItemByKey($quoteItem->getId()); } } } }