33 * Copyright © Magento, Inc. All rights reserved.
44 * See COPYING.txt for license details.
55 */
6+ declare (strict_types=1 );
7+
68namespace Magento \Quote \Model \ResourceModel \Quote \Item ;
79
8- use \Magento \Catalog \Model \ResourceModel \Product \Collection as ProductCollection ;
10+ use Magento \Catalog \Api \Data \ProductInterface ;
11+ use Magento \Catalog \Model \ResourceModel \Product \Collection as ProductCollection ;
12+ use Magento \Catalog \Model \Product \Attribute \Source \Status as ProductStatus ;
13+ use Magento \Quote \Model \Quote ;
14+ use Magento \Quote \Model \Quote \Item as QuoteItem ;
15+ use Magento \Quote \Model \ResourceModel \Quote \Item as ResourceQuoteItem ;
916
1017/**
1118 * Quote item resource collection
@@ -50,6 +57,11 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\VersionContro
5057 */
5158 private $ storeManager ;
5259
60+ /**
61+ * @var bool $recollectQuote
62+ */
63+ private $ recollectQuote = false ;
64+
5365 /**
5466 * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory
5567 * @param \Psr\Log\LoggerInterface $logger
@@ -102,15 +114,15 @@ public function __construct(
102114 */
103115 protected function _construct ()
104116 {
105- $ this ->_init (\ Magento \ Quote \ Model \ Quote \Item ::class, \ Magento \ Quote \ Model \ ResourceModel \ Quote \Item ::class);
117+ $ this ->_init (QuoteItem ::class, ResourceQuoteItem ::class);
106118 }
107119
108120 /**
109121 * Retrieve store Id (From Quote)
110122 *
111123 * @return int
112124 */
113- public function getStoreId ()
125+ public function getStoreId (): int
114126 {
115127 // Fallback to current storeId if no quote is provided
116128 // (see https://github.com/magento/magento2/commit/9d3be732a88884a66d667b443b3dc1655ddd0721)
@@ -119,12 +131,12 @@ public function getStoreId()
119131 }
120132
121133 /**
122- * Set Quote object to Collection
134+ * Set Quote object to Collection.
123135 *
124- * @param \Magento\Quote\Model\ Quote $quote
136+ * @param Quote $quote
125137 * @return $this
126138 */
127- public function setQuote ($ quote )
139+ public function setQuote ($ quote ): self
128140 {
129141 $ this ->_quote = $ quote ;
130142 $ quoteId = $ quote ->getId ();
@@ -138,13 +150,15 @@ public function setQuote($quote)
138150 }
139151
140152 /**
141- * Reset the collection and join it to quotes table. Optionally can select items with specified product id only.
153+ * Reset the collection and inner join it to quotes table.
154+ *
155+ * Optionally can select items with specified product id only
142156 *
143157 * @param string $quotesTableName
144158 * @param int $productId
145159 * @return $this
146160 */
147- public function resetJoinQuotes ($ quotesTableName , $ productId = null )
161+ public function resetJoinQuotes ($ quotesTableName , $ productId = null ): self
148162 {
149163 $ this ->getSelect ()->reset ()->from (
150164 ['qi ' => $ this ->getResource ()->getMainTable ()],
@@ -161,11 +175,11 @@ public function resetJoinQuotes($quotesTableName, $productId = null)
161175 }
162176
163177 /**
164- * After load processing
178+ * After load processing.
165179 *
166180 * @return $this
167181 */
168- protected function _afterLoad ()
182+ protected function _afterLoad (): self
169183 {
170184 parent ::_afterLoad ();
171185
@@ -194,11 +208,11 @@ protected function _afterLoad()
194208 }
195209
196210 /**
197- * Add options to items
211+ * Add options to items.
198212 *
199213 * @return $this
200214 */
201- protected function _assignOptions ()
215+ protected function _assignOptions (): self
202216 {
203217 $ itemIds = array_keys ($ this ->_items );
204218 $ optionCollection = $ this ->_itemOptionCollectionFactory ->create ()->addItemFilter ($ itemIds );
@@ -212,12 +226,12 @@ protected function _assignOptions()
212226 }
213227
214228 /**
215- * Add products to items and item options
229+ * Add products to items and item options.
216230 *
217231 * @return $this
218232 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
219233 */
220- protected function _assignProducts ()
234+ protected function _assignProducts (): self
221235 {
222236 \Magento \Framework \Profiler::start ('QUOTE: ' . __METHOD__ , ['group ' => 'QUOTE ' , 'method ' => __METHOD__ ]);
223237 $ productCollection = $ this ->_productCollectionFactory ->create ()->setStoreId (
@@ -239,53 +253,88 @@ protected function _assignProducts()
239253 ['collection ' => $ productCollection ]
240254 );
241255
242- $ recollectQuote = false ;
243256 foreach ($ this as $ item ) {
257+ /** @var ProductInterface $product */
244258 $ product = $ productCollection ->getItemById ($ item ->getProductId ());
245- if ($ product ) {
259+ $ isValidProduct = $ this ->isValidProduct ($ product );
260+ $ qtyOptions = [];
261+ if ($ isValidProduct ) {
246262 $ product ->setCustomOptions ([]);
247- $ qtyOptions = [];
248- $ optionProductIds = [];
249- foreach ($ item ->getOptions () as $ option ) {
250- /**
251- * Call type-specific logic for product associated with quote item
252- */
253- $ product ->getTypeInstance ()->assignProductToOption (
254- $ productCollection ->getItemById ($ option ->getProductId ()),
255- $ option ,
256- $ product
257- );
258-
259- if (is_object ($ option ->getProduct ()) && $ option ->getProduct ()->getId () != $ product ->getId ()) {
260- $ optionProductIds [$ option ->getProduct ()->getId ()] = $ option ->getProduct ()->getId ();
261- }
262- }
263-
264- if ($ optionProductIds ) {
265- foreach ($ optionProductIds as $ optionProductId ) {
266- $ qtyOption = $ item ->getOptionByCode ('product_qty_ ' . $ optionProductId );
267- if ($ qtyOption ) {
268- $ qtyOptions [$ optionProductId ] = $ qtyOption ;
269- }
263+ $ optionProductIds = $ this ->getOptionProductIds ($ item , $ product , $ productCollection );
264+ foreach ($ optionProductIds as $ optionProductId ) {
265+ $ qtyOption = $ item ->getOptionByCode ('product_qty_ ' . $ optionProductId );
266+ if ($ qtyOption ) {
267+ $ qtyOptions [$ optionProductId ] = $ qtyOption ;
270268 }
271269 }
272-
273- $ item ->setQtyOptions ($ qtyOptions )->setProduct ($ product );
274270 } else {
275271 $ item ->isDeleted (true );
276- $ recollectQuote = true ;
272+ $ this ->recollectQuote = true ;
273+ }
274+ if (!$ item ->isDeleted ()) {
275+ $ item ->setQtyOptions ($ qtyOptions )->setProduct ($ product );
276+ $ item ->checkData ();
277277 }
278- $ item ->checkData ();
279278 }
280-
281- if ($ recollectQuote && $ this ->_quote ) {
279+ if ($ this ->recollectQuote && $ this ->_quote ) {
282280 $ this ->_quote ->collectTotals ();
283281 }
284282 \Magento \Framework \Profiler::stop ('QUOTE: ' . __METHOD__ );
285283
286284 return $ this ;
287285 }
288286
287+ /**
288+ * Get product Ids from option.
289+ *
290+ * @param QuoteItem $item
291+ * @param ProductInterface $product
292+ * @param ProductCollection $productCollection
293+ * @return array
294+ */
295+ private function getOptionProductIds (
296+ QuoteItem $ item ,
297+ ProductInterface $ product ,
298+ ProductCollection $ productCollection
299+ ): array {
300+ $ optionProductIds = [];
301+ foreach ($ item ->getOptions () as $ option ) {
302+ /**
303+ * Call type-specific logic for product associated with quote item
304+ */
305+ $ product ->getTypeInstance ()->assignProductToOption (
306+ $ productCollection ->getItemById ($ option ->getProductId ()),
307+ $ option ,
308+ $ product
309+ );
310+
311+ if (is_object ($ option ->getProduct ()) && $ option ->getProduct ()->getId () != $ product ->getId ()) {
312+ $ isValidProduct = $ this ->isValidProduct ($ option ->getProduct ());
313+ if (!$ isValidProduct && !$ item ->isDeleted ()) {
314+ $ item ->isDeleted (true );
315+ $ this ->recollectQuote = true ;
316+ continue ;
317+ }
318+ $ optionProductIds [$ option ->getProduct ()->getId ()] = $ option ->getProduct ()->getId ();
319+ }
320+ }
321+
322+ return $ optionProductIds ;
323+ }
324+
325+ /**
326+ * Check is valid product.
327+ *
328+ * @param ProductInterface $product
329+ * @return bool
330+ */
331+ private function isValidProduct (ProductInterface $ product ): bool
332+ {
333+ $ result = ($ product && (int )$ product ->getStatus () !== ProductStatus::STATUS_DISABLED );
334+
335+ return $ result ;
336+ }
337+
289338 /**
290339 * Prevents adding stock status filter to the collection of products.
291340 *
@@ -294,7 +343,7 @@ protected function _assignProducts()
294343 *
295344 * @see \Magento\CatalogInventory\Helper\Stock::addIsInStockFilterToCollection
296345 */
297- private function skipStockStatusFilter (ProductCollection $ productCollection )
346+ private function skipStockStatusFilter (ProductCollection $ productCollection ): void
298347 {
299348 $ productCollection ->setFlag ('has_stock_status_filter ' , true );
300349 }
@@ -304,7 +353,7 @@ private function skipStockStatusFilter(ProductCollection $productCollection)
304353 *
305354 * @return void
306355 */
307- private function removeItemsWithAbsentProducts ()
356+ private function removeItemsWithAbsentProducts (): void
308357 {
309358 if (count ($ this ->_productIds ) === 0 ) {
310359 return ;
0 commit comments