diff --git a/src/Adapter/Product/Combination/Create/CombinationCreator.php b/src/Adapter/Product/Combination/Create/CombinationCreator.php index 8844f643d1030..96e48f465914b 100644 --- a/src/Adapter/Product/Combination/Create/CombinationCreator.php +++ b/src/Adapter/Product/Combination/Create/CombinationCreator.php @@ -42,6 +42,7 @@ use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductId; use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductType; use PrestaShop\PrestaShop\Core\Domain\Shop\Exception\ShopAssociationNotFound; +use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopCollection; use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopConstraint; use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopId; use PrestaShop\PrestaShop\Core\Exception\CoreException; @@ -166,7 +167,7 @@ private function syncOutOfStockType( $productStockAvailable = $this->stockAvailableRepository->getForProduct($productId, new ShopId($product->getShopId())); $outOfStockType = new OutOfStockType((int) $productStockAvailable->out_of_stock); - if ($shopConstraint->forAllShops()) { + if ($shopConstraint->forAllShops() || ($shopConstraint instanceof ShopCollection && $shopConstraint->hasShopIds())) { foreach ($shopIdsByConstraint as $shopId) { $this->combinationRepository->updateCombinationOutOfStockType($productId, $outOfStockType, ShopConstraint::shop($shopId->getValue())); } diff --git a/src/Adapter/Product/Combination/Repository/CombinationRepository.php b/src/Adapter/Product/Combination/Repository/CombinationRepository.php index ea682962c2704..da5b6c2d8a3ce 100644 --- a/src/Adapter/Product/Combination/Repository/CombinationRepository.php +++ b/src/Adapter/Product/Combination/Repository/CombinationRepository.php @@ -29,6 +29,7 @@ use Combination; use Db; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Exception; use PrestaShop\PrestaShop\Adapter\Attribute\Repository\AttributeRepository; @@ -49,6 +50,7 @@ use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductId; use PrestaShop\PrestaShop\Core\Domain\Shop\Exception\InvalidShopConstraintException; use PrestaShop\PrestaShop\Core\Domain\Shop\Exception\ShopException; +use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopCollection; use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopConstraint; use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopGroupId; use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopId; @@ -276,27 +278,44 @@ public function copyToShop(CombinationId $combinationId, ShopId $sourceId, ShopI */ public function getByShopConstraint(CombinationId $combinationId, ShopConstraint $shopConstraint): Combination { - if ($shopConstraint->getShopGroupId()) { - throw new InvalidShopConstraintException('Combination has no features related with shop group use single shop and all shops constraints'); - } - - if ($shopConstraint->forAllShops()) { + if ($shopConstraint->forAllShops() || ($shopConstraint instanceof ShopCollection && $shopConstraint->hasShopIds())) { try { return $this->get($combinationId, $this->getDefaultShopIdForCombination($combinationId)); + } catch (CombinationShopAssociationNotFoundException $e) { // We try to fetch combination for default shop first, // but in case it is not associated to default shop, // then we load first found associated combination - } catch (CombinationShopAssociationNotFoundException $e) { $associatedShopIds = $this->getAssociatedShopIds($combinationId); if (empty($associatedShopIds)) { throw $e; } - return $this->get($combinationId, reset($associatedShopIds)); + if ($shopConstraint instanceof ShopCollection) { + $defaultShopId = null; + // Find first shop IDs that is both in the specified list and the valid associated shops + $validShopIds = array_map(fn (ShopId $shopId) => $shopId->getValue(), $associatedShopIds); + foreach ($shopConstraint->getShopIds() as $shopId) { + if (in_array($shopId, $validShopIds)) { + $defaultShopId = $shopId; + break; + } + } + + // If none is found, it means no provided shop IDs were associated so we trigger the exception + if (empty($defaultShopId)) { + throw $e; + } + } else { + $defaultShopId = reset($associatedShopIds); + } + + return $this->get($combinationId, $defaultShopId); } - } else { + } elseif ($shopConstraint->getShopId()) { return $this->get($combinationId, $shopConstraint->getShopId()); } + + throw new InvalidShopConstraintException('Combination has no features related with shop group use single shop, shop collection and all shops constraints'); } /** @@ -308,7 +327,7 @@ public function getByShopConstraint(CombinationId $combinationId, ShopConstraint public function partialUpdate(Combination $combination, array $updatableProperties, ShopConstraint $shopConstraint, int $errorCode): void { if ($shopConstraint->getShopGroupId()) { - throw new InvalidShopConstraintException('Product combination has no features related with shop group use single shop and all shops constraints'); + throw new InvalidShopConstraintException('Product Combination has no features related with shop group use single shop, shop collection and all shops constraints'); } $this->combinationValidator->validate($combination); @@ -459,11 +478,13 @@ function (array $combination) { return new CombinationId((int) $combination['id_ public function findFirstCombinationId(ProductId $productId, ShopConstraint $shopConstraint): ?CombinationId { if ($shopConstraint->getShopGroupId()) { - throw new InvalidShopConstraintException('Combination has no features related with shop group use single shop and all shops constraints'); + throw new InvalidShopConstraintException('Combination has no features related with shop group use single shop, shop collection and all shops constraints'); } if ($shopConstraint->getShopId()) { $shopId = $shopConstraint->getShopId(); + } elseif ($shopConstraint instanceof ShopCollection && $shopConstraint->hasShopIds()) { + $shopId = $shopConstraint->getShopIds()[0]; } else { $shopId = $this->productRepository->getProductDefaultShopId($productId); } @@ -761,6 +782,10 @@ private function getShopIdsByConstraint(CombinationId $combinationId, ShopConstr return $this->getAssociatedShopIds($combinationId); } + if ($shopConstraint instanceof ShopCollection && $shopConstraint->hasShopIds()) { + return $shopConstraint->getShopIds(); + } + return [$shopConstraint->getShopId()]; } @@ -801,13 +826,29 @@ private function searchCombinationIdsByAttributes( 'pa', 'pac.id_product_attribute = pa.id_product_attribute' ); - } else { + } elseif ($shopConstraint instanceof ShopCollection && $shopConstraint->hasShopIds()) { + $qb + ->innerJoin( + 'pac', + $this->dbPrefix . 'product_attribute_shop', + 'pa', + 'pac.id_product_attribute = pa.id_product_attribute AND pa.id_shop IN (:shopIds)' + ) + ->setParameter( + 'shopIds', + array_map(fn (ShopId $shopId) => $shopId->getValue(), $shopConstraint->getShopIds()), + ArrayParameterType::INTEGER + ) + ; + } elseif ($shopConstraint->getShopId()) { $qb->innerJoin( 'pac', $this->dbPrefix . 'product_attribute_shop', 'pa', 'pac.id_product_attribute = pa.id_product_attribute AND pa.id_shop = :shopId' )->setParameter('shopId', $shopConstraint->getShopId()->getValue()); + } else { + throw new InvalidShopConstraintException('Cannot handle this type of ShopConstraint'); } $qb @@ -870,12 +911,13 @@ private function searchAttributes(LanguageId $languageId, ShopConstraint $shopCo if ($shopConstraint->getShopId()) { // this makes sure we are searching only in certain shop, so it doesn't return irrelevant attribute ids - $qb->innerJoin( - 'a', - $this->dbPrefix . 'attribute_shop', - 'attrShop', - 'a.id_attribute = attrShop.id_attribute AND attrShop.id_shop = :shopId' - ) + $qb + ->innerJoin( + 'a', + $this->dbPrefix . 'attribute_shop', + 'attrShop', + 'a.id_attribute = attrShop.id_attribute AND attrShop.id_shop = :shopId' + ) ->innerJoin( 'agl', $this->dbPrefix . 'attribute_group_shop', 'ags', @@ -885,6 +927,28 @@ private function searchAttributes(LanguageId $languageId, ShopConstraint $shopCo ; } + if ($shopConstraint instanceof ShopCollection && $shopConstraint->hasShopIds()) { + // this makes sure we are searching only in certain shop, so it doesn't return irrelevant attribute ids + $qb + ->innerJoin( + 'a', + $this->dbPrefix . 'attribute_shop', + 'attrShop', + 'a.id_attribute = attrShop.id_attribute AND attrShop.id_shop IN (:shopIds)' + ) + ->innerJoin( + 'agl', + $this->dbPrefix . 'attribute_group_shop', 'ags', + 'agl.id_attribute_group = ags.id_attribute_group AND ags.id_shop IN (:shopIds)' + ) + ->setParameter( + 'shopIds', + array_map(fn (ShopId $shopId) => $shopId->getValue(), $shopConstraint->getShopIds()), + ArrayParameterType::INTEGER + ) + ; + } + $results = $qb->executeQuery()->fetchAllAssociative(); return array_map('intval', array_column($results, 'id_attribute')); diff --git a/src/Adapter/Product/Combination/Update/CombinationStockUpdater.php b/src/Adapter/Product/Combination/Update/CombinationStockUpdater.php index bdac005c3097a..e2bca881b1fe2 100644 --- a/src/Adapter/Product/Combination/Update/CombinationStockUpdater.php +++ b/src/Adapter/Product/Combination/Update/CombinationStockUpdater.php @@ -37,6 +37,7 @@ use PrestaShop\PrestaShop\Core\Domain\Product\Combination\ValueObject\CombinationId; use PrestaShop\PrestaShop\Core\Domain\Product\Stock\ValueObject\StockId; use PrestaShop\PrestaShop\Core\Domain\Product\Stock\ValueObject\StockModification; +use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopCollection; use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopConstraint; use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopId; use PrestaShop\PrestaShop\Core\Hook\HookDispatcherInterface; @@ -189,11 +190,16 @@ private function updateStockByShopConstraint( ShopConstraint $shopConstraint ): void { $combinationId = new CombinationId((int) $combination->id); - if ($shopConstraint->forAllShops()) { + if ($shopConstraint->forAllShops() || ($shopConstraint instanceof ShopCollection && $shopConstraint->hasShopIds())) { // Since each stock has a distinct ID we can't use the ObjectModel multi shop feature based on id_shop_list, // so we manually loop to update each associated stocks - $shops = $this->combinationRepository->getAssociatedShopIds($combinationId); - foreach ($shops as $shopId) { + if ($shopConstraint instanceof ShopCollection) { + $shopIds = $shopConstraint->getShopIds(); + } else { + $shopIds = $this->combinationRepository->getAssociatedShopIds($combinationId); + } + + foreach ($shopIds as $shopId) { $this->updateStockAvailable( $this->stockAvailableRepository->getForCombination($combinationId, $shopId), $properties diff --git a/src/Adapter/Product/Image/Repository/ProductImageRepository.php b/src/Adapter/Product/Image/Repository/ProductImageRepository.php index 8d6dd804fdda0..90c7e64a4d8ab 100644 --- a/src/Adapter/Product/Image/Repository/ProductImageRepository.php +++ b/src/Adapter/Product/Image/Repository/ProductImageRepository.php @@ -122,6 +122,12 @@ public function getImages(ProductId $productId, ShopConstraint $shopConstraint): $this->productRepository->assertProductIsAssociatedToShop($productId, $shopConstraint->getShopId()); } + if ($shopConstraint instanceof ShopCollection && $shopConstraint->hasShopIds()) { + foreach ($shopConstraint->getShopIds() as $shopId) { + $this->productRepository->assertProductIsAssociatedToShop($productId, $shopId); + } + } + return array_map( function (ImageId $imageId) use ($shopConstraint): Image { return $this->getByShopConstraint($imageId, $shopConstraint); @@ -168,11 +174,22 @@ public function getImageIds(ProductId $productId, ShopConstraint $shopConstraint ) ->setParameter('shopGroupId', $shopConstraint->getShopGroupId()->getValue()) ; - } else { + } elseif ($shopConstraint instanceof ShopCollection && $shopConstraint->hasShopIds()) { + $qb + ->andWhere('img_shop.id_shop IN (:shopIds)') + ->setParameter( + 'shopIds', + array_map(fn (ShopId $shopId) => $shopId->getValue(), $shopConstraint->getShopIds()), + ArrayParameterType::INTEGER + ) + ; + } elseif ($shopConstraint->getShopId()) { $this->productRepository->assertProductIsAssociatedToShop($productId, $shopConstraint->getShopId()); $qb->andWhere('img_shop.id_shop = :shopId') ->setParameter('shopId', $shopConstraint->getShopId()->getValue()) ; + } else { + throw new InvalidShopConstraintException('Cannot handle this type of ShopConstraint'); } } diff --git a/src/Adapter/Product/Pack/Repository/ProductPackRepository.php b/src/Adapter/Product/Pack/Repository/ProductPackRepository.php index f8672e4629ae8..b500209e33f91 100644 --- a/src/Adapter/Product/Pack/Repository/ProductPackRepository.php +++ b/src/Adapter/Product/Pack/Repository/ProductPackRepository.php @@ -79,7 +79,7 @@ public function __construct( */ public function getPackedProducts(PackId $productId, LanguageId $languageId, ShopConstraint $shopConstraint): array { - if ($shopConstraint->getShopGroupId() || $shopConstraint->forAllShops()) { + if (!$shopConstraint->isSingleShopContext()) { throw new InvalidShopConstraintException('Product Pack has no features related with shop group or all shops, use single shop constraint'); } diff --git a/src/Adapter/Product/Stock/Update/ProductStockUpdater.php b/src/Adapter/Product/Stock/Update/ProductStockUpdater.php index 284c6c32a23e7..735f6e977f1a8 100644 --- a/src/Adapter/Product/Stock/Update/ProductStockUpdater.php +++ b/src/Adapter/Product/Stock/Update/ProductStockUpdater.php @@ -141,12 +141,14 @@ public function resetStock(ProductId $productId, ShopConstraint $shopConstraint) } if ($shopConstraint->forAllShops()) { - $shops = $this->productRepository->getAssociatedShopIds($productId); + $shopIds = $this->productRepository->getAssociatedShopIds($productId); + } elseif ($shopConstraint instanceof ShopCollection && $shopConstraint->hasShopIds()) { + $shopIds = $shopConstraint->getShopIds(); } else { - $shops = [$shopConstraint->getShopId()]; + $shopIds = [$shopConstraint->getShopId()]; } - foreach ($shops as $shopId) { + foreach ($shopIds as $shopId) { $stockAvailable = $this->stockAvailableRepository->getForProduct($productId, $shopId); if ((int) $stockAvailable->quantity === 0) { continue; diff --git a/src/Adapter/Product/Update/ProductIndexationUpdater.php b/src/Adapter/Product/Update/ProductIndexationUpdater.php index 6481047afc938..62fe4336edd21 100644 --- a/src/Adapter/Product/Update/ProductIndexationUpdater.php +++ b/src/Adapter/Product/Update/ProductIndexationUpdater.php @@ -31,6 +31,7 @@ use PrestaShop\PrestaShop\Adapter\ContextStateManager; use PrestaShop\PrestaShop\Core\Domain\Product\Exception\CannotUpdateProductException; use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductVisibility; +use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopCollection; use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopConstraint; use PrestaShop\PrestaShop\Core\Exception\CoreException; use PrestaShopException; @@ -102,12 +103,14 @@ public function updateIndexation(Product $product, ShopConstraint $shopConstrain private function updateProductIndexes(int $productId, ShopConstraint $shopConstraint): void { try { - $this->adaptShopContext($shopConstraint); - if (!Search::indexation(false, $productId)) { - throw new CannotUpdateProductException( - sprintf('Cannot update search indexes for product %d', $productId), - CannotUpdateProductException::FAILED_UPDATE_SEARCH_INDEXATION - ); + // If a specific list is provided we update them one by one + if ($shopConstraint instanceof ShopCollection && $shopConstraint->hasShopIds()) { + foreach ($shopConstraint->getShopIds() as $shopId) { + $this->updateProductIndexesByShopConstraint($productId, ShopConstraint::shop($shopId->getValue())); + } + } else { + // If not the other types of ShopConstraint are handled by this method + $this->updateProductIndexesByShopConstraint($productId, $shopConstraint); } } catch (PrestaShopException $e) { throw new CoreException( @@ -120,6 +123,17 @@ private function updateProductIndexes(int $productId, ShopConstraint $shopConstr } } + private function updateProductIndexesByShopConstraint(int $productId, ShopConstraint $shopConstraint): void + { + $this->adaptShopContext($shopConstraint); + if (!Search::indexation(false, $productId)) { + throw new CannotUpdateProductException( + sprintf('Cannot update search indexes for product %d', $productId), + CannotUpdateProductException::FAILED_UPDATE_SEARCH_INDEXATION + ); + } + } + /** * @param int $productId * @param ShopConstraint $shopConstraint @@ -129,8 +143,17 @@ private function updateProductIndexes(int $productId, ShopConstraint $shopConstr private function removeProductIndexes(int $productId, ShopConstraint $shopConstraint): void { try { - $this->adaptShopContext($shopConstraint); - Search::removeProductsSearchIndex([$productId]); + // If a specific list is provided we update them one by one + if ($shopConstraint instanceof ShopCollection && $shopConstraint->hasShopIds()) { + foreach ($shopConstraint->getShopIds() as $shopId) { + $this->adaptShopContext(ShopConstraint::shop($shopId->getValue())); + Search::removeProductsSearchIndex([$productId]); + } + } else { + // If not the other types of ShopConstraint are handled by this method + $this->adaptShopContext($shopConstraint); + Search::removeProductsSearchIndex([$productId]); + } } catch (PrestaShopException $e) { throw new CoreException( sprintf('Error occurred while removing search indexes for product %d', $productId), diff --git a/src/Core/Repository/ShopConstraintTrait.php b/src/Core/Repository/ShopConstraintTrait.php index 02b61cd8c9c49..52a8f9ea09365 100644 --- a/src/Core/Repository/ShopConstraintTrait.php +++ b/src/Core/Repository/ShopConstraintTrait.php @@ -28,8 +28,11 @@ namespace PrestaShop\PrestaShop\Core\Repository; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Query\QueryBuilder; +use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopCollection; use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopConstraint; +use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopId; trait ShopConstraintTrait { @@ -55,6 +58,17 @@ protected function applyShopConstraint( ; } + if ($shopConstraint instanceof ShopCollection && $shopConstraint->hasShopIds()) { + $queryBuilder + ->andWhere('id_shop IN (:shopIds)') + ->setParameter( + 'shopIds', + array_map(fn (ShopId $shopId) => $shopId->getValue(), $shopConstraint->getShopIds()), + ArrayParameterType::INTEGER + ) + ; + } + return $queryBuilder; } }