1<?php 2/** 3 * Copyright since 2007 PrestaShop SA and Contributors 4 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA 5 * 6 * NOTICE OF LICENSE 7 * 8 * This source file is subject to the Open Software License (OSL 3.0) 9 * that is bundled with this package in the file LICENSE.md. 10 * It is also available through the world-wide-web at this URL: 11 * https://opensource.org/licenses/OSL-3.0 12 * If you did not receive a copy of the license and are unable to 13 * obtain it through the world-wide-web, please send an email 14 * to license@prestashop.com so we can send you a copy immediately. 15 * 16 * DISCLAIMER 17 * 18 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer 19 * versions in the future. If you wish to customize PrestaShop for your 20 * needs please refer to https://devdocs.prestashop.com/ for more information. 21 * 22 * @author PrestaShop SA and Contributors <contact@prestashop.com> 23 * @copyright Since 2007 PrestaShop SA and Contributors 24 * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) 25 */ 26 27declare(strict_types=1); 28 29namespace PrestaShop\PrestaShop\Adapter\Product\Repository; 30 31use Doctrine\DBAL\Connection; 32use PrestaShop\Decimal\DecimalNumber; 33use PrestaShop\PrestaShop\Adapter\AbstractObjectModelRepository; 34use PrestaShop\PrestaShop\Adapter\Manufacturer\Repository\ManufacturerRepository; 35use PrestaShop\PrestaShop\Adapter\Product\Validate\ProductValidator; 36use PrestaShop\PrestaShop\Adapter\TaxRulesGroup\Repository\TaxRulesGroupRepository; 37use PrestaShop\PrestaShop\Core\Domain\Language\ValueObject\LanguageId; 38use PrestaShop\PrestaShop\Core\Domain\Manufacturer\ValueObject\ManufacturerId; 39use PrestaShop\PrestaShop\Core\Domain\Manufacturer\ValueObject\NoManufacturerId; 40use PrestaShop\PrestaShop\Core\Domain\Product\Exception\CannotAddProductException; 41use PrestaShop\PrestaShop\Core\Domain\Product\Exception\CannotBulkDeleteProductException; 42use PrestaShop\PrestaShop\Core\Domain\Product\Exception\CannotDeleteProductException; 43use PrestaShop\PrestaShop\Core\Domain\Product\Exception\CannotDuplicateProductException; 44use PrestaShop\PrestaShop\Core\Domain\Product\Exception\CannotUpdateProductException; 45use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductConstraintException; 46use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductException; 47use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductNotFoundException; 48use PrestaShop\PrestaShop\Core\Domain\Product\Pack\Exception\ProductPackConstraintException; 49use PrestaShop\PrestaShop\Core\Domain\Product\ProductTaxRulesGroupSettings; 50use PrestaShop\PrestaShop\Core\Domain\Product\Stock\Exception\ProductStockConstraintException; 51use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductId; 52use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductType; 53use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopId; 54use PrestaShop\PrestaShop\Core\Domain\TaxRulesGroup\ValueObject\TaxRulesGroupId; 55use PrestaShop\PrestaShop\Core\Exception\CoreException; 56use PrestaShopException; 57use Product; 58 59/** 60 * Methods to access data storage for Product 61 */ 62class ProductRepository extends AbstractObjectModelRepository 63{ 64 /** 65 * @var Connection 66 */ 67 private $connection; 68 69 /** 70 * @var string 71 */ 72 private $dbPrefix; 73 74 /** 75 * @var ProductValidator 76 */ 77 private $productValidator; 78 79 /** 80 * @var int 81 */ 82 private $defaultCategoryId; 83 84 /** 85 * @var TaxRulesGroupRepository 86 */ 87 private $taxRulesGroupRepository; 88 89 /** 90 * @var ManufacturerRepository 91 */ 92 private $manufacturerRepository; 93 94 /** 95 * @param Connection $connection 96 * @param string $dbPrefix 97 * @param ProductValidator $productValidator 98 * @param int $defaultCategoryId 99 * @param TaxRulesGroupRepository $taxRulesGroupRepository 100 * @param ManufacturerRepository $manufacturerRepository 101 */ 102 public function __construct( 103 Connection $connection, 104 string $dbPrefix, 105 ProductValidator $productValidator, 106 int $defaultCategoryId, 107 TaxRulesGroupRepository $taxRulesGroupRepository, 108 ManufacturerRepository $manufacturerRepository 109 ) { 110 $this->connection = $connection; 111 $this->dbPrefix = $dbPrefix; 112 $this->productValidator = $productValidator; 113 $this->defaultCategoryId = $defaultCategoryId; 114 $this->taxRulesGroupRepository = $taxRulesGroupRepository; 115 $this->manufacturerRepository = $manufacturerRepository; 116 } 117 118 /** 119 * Duplicates product entity without relations 120 * 121 * @param Product $product 122 * 123 * @return Product 124 * 125 * @throws CoreException 126 * @throws CannotDuplicateProductException 127 * @throws ProductConstraintException 128 * @throws ProductException 129 */ 130 public function duplicate(Product $product): Product 131 { 132 unset($product->id, $product->id_product); 133 134 $this->productValidator->validateCreation($product); 135 $this->productValidator->validate($product); 136 $this->addObjectModel($product, CannotDuplicateProductException::class); 137 138 return $product; 139 } 140 141 /** 142 * Gets product price by provided shop 143 * 144 * @param ProductId $productId 145 * @param ShopId $shopId 146 * 147 * @return DecimalNumber|null 148 */ 149 public function getPriceByShop(ProductId $productId, ShopId $shopId): ?DecimalNumber 150 { 151 $qb = $this->connection->createQueryBuilder(); 152 $qb->select('price') 153 ->from($this->dbPrefix . 'product_shop') 154 ->where('id_product = :productId') 155 ->andWhere('id_shop = :shopId') 156 ->setParameter('productId', $productId->getValue()) 157 ->setParameter('shopId', $shopId->getValue()) 158 ; 159 160 $result = $qb->execute()->fetch(); 161 162 if (!$result) { 163 return null; 164 } 165 166 return new DecimalNumber($result['price']); 167 } 168 169 /** 170 * @param ProductId $productId 171 */ 172 public function assertProductExists(ProductId $productId): void 173 { 174 $this->assertObjectModelExists($productId->getValue(), 'product', ProductNotFoundException::class); 175 } 176 177 /** 178 * @param ProductId[] $productIds 179 * 180 * @throws ProductNotFoundException 181 */ 182 public function assertAllProductsExists(array $productIds): void 183 { 184 //@todo: no shop association. Should it be checked here? 185 $ids = array_map(function (ProductId $productId): int { 186 return $productId->getValue(); 187 }, $productIds); 188 $ids = array_unique($ids); 189 190 $qb = $this->connection->createQueryBuilder(); 191 $qb->select('COUNT(id_product) as product_count') 192 ->from($this->dbPrefix . 'product') 193 ->where('id_product IN (:productIds)') 194 ->setParameter('productIds', $ids, Connection::PARAM_INT_ARRAY) 195 ; 196 197 $results = $qb->execute()->fetch(); 198 199 if (!$results || (int) $results['product_count'] !== count($ids)) { 200 throw new ProductNotFoundException( 201 sprintf( 202 'Some of these products do not exist: %s', 203 implode(',', $ids) 204 ) 205 ); 206 } 207 } 208 209 /** 210 * @param ProductId $productId 211 * @param LanguageId $languageId 212 * 213 * @return array<array<string, string>> 214 * e.g [ 215 * ['id_product' => '1', 'name' => 'Product name', 'reference' => 'demo15'], 216 * ['id_product' => '2', 'name' => 'Product name2', 'reference' => 'demo16'], 217 * ] 218 * 219 * @throws CoreException 220 */ 221 public function getRelatedProducts(ProductId $productId, LanguageId $languageId): array 222 { 223 $this->assertProductExists($productId); 224 $productIdValue = $productId->getValue(); 225 226 try { 227 $accessories = Product::getAccessoriesLight($languageId->getValue(), $productIdValue); 228 } catch (PrestaShopException $e) { 229 throw new CoreException(sprintf( 230 'Error occurred when fetching related products for product #%d', 231 $productIdValue 232 )); 233 } 234 235 return $accessories; 236 } 237 238 /** 239 * @param ProductId $productId 240 * 241 * @return Product 242 * 243 * @throws CoreException 244 */ 245 public function get(ProductId $productId): Product 246 { 247 /** @var Product $product */ 248 $product = $this->getObjectModel( 249 $productId->getValue(), 250 Product::class, 251 ProductNotFoundException::class 252 ); 253 254 try { 255 $product->loadStockData(); 256 } catch (PrestaShopException $e) { 257 throw new CoreException( 258 sprintf('Error occurred when trying to load Product stock #%d', $productId->getValue()), 259 0, 260 $e 261 ); 262 } 263 264 return $product; 265 } 266 267 /** 268 * @param array<int, string> $localizedNames 269 * @param string $productType 270 * 271 * @return Product 272 * 273 * @throws CannotAddProductException 274 */ 275 public function create(array $localizedNames, string $productType): Product 276 { 277 $product = new Product(); 278 $product->active = false; 279 $product->id_category_default = $this->defaultCategoryId; 280 $product->name = $localizedNames; 281 $product->is_virtual = ProductType::TYPE_VIRTUAL === $productType; 282 $product->cache_is_pack = ProductType::TYPE_PACK === $productType; 283 $product->product_type = $productType; 284 285 $this->productValidator->validateCreation($product); 286 $this->addObjectModel($product, CannotAddProductException::class); 287 $product->addToCategories([$product->id_category_default]); 288 289 return $product; 290 } 291 292 /** 293 * @param Product $product 294 * @param array $propertiesToUpdate 295 * @param int $errorCode 296 * 297 * @throws CoreException 298 * @throws ProductConstraintException 299 * @throws ProductPackConstraintException 300 * @throws ProductStockConstraintException 301 */ 302 public function partialUpdate(Product $product, array $propertiesToUpdate, int $errorCode): void 303 { 304 $taxRulesGroupIdIsBeingUpdated = in_array('id_tax_rules_group', $propertiesToUpdate, true); 305 $taxRulesGroupId = (int) $product->id_tax_rules_group; 306 $manufacturerIdIsBeingUpdated = in_array('id_manufacturer', $propertiesToUpdate, true); 307 $manufacturerId = (int) $product->id_manufacturer; 308 309 if ($taxRulesGroupIdIsBeingUpdated && $taxRulesGroupId !== ProductTaxRulesGroupSettings::NONE_APPLIED) { 310 $this->taxRulesGroupRepository->assertTaxRulesGroupExists(new TaxRulesGroupId($taxRulesGroupId)); 311 } 312 if ($manufacturerIdIsBeingUpdated && $manufacturerId !== NoManufacturerId::NO_MANUFACTURER_ID) { 313 $this->manufacturerRepository->assertManufacturerExists(new ManufacturerId($manufacturerId)); 314 } 315 316 $this->productValidator->validate($product); 317 $this->partiallyUpdateObjectModel( 318 $product, 319 $propertiesToUpdate, 320 CannotUpdateProductException::class, 321 $errorCode 322 ); 323 } 324 325 /** 326 * @param ProductId $productId 327 * 328 * @throws CoreException 329 */ 330 public function delete(ProductId $productId): void 331 { 332 $this->deleteObjectModel($this->get($productId), CannotDeleteProductException::class); 333 } 334 335 /** 336 * @param array $productIds 337 * 338 * @throws CannotBulkDeleteProductException 339 */ 340 public function bulkDelete(array $productIds): void 341 { 342 $failedIds = []; 343 foreach ($productIds as $productId) { 344 try { 345 $this->delete($productId); 346 } catch (CannotDeleteProductException $e) { 347 $failedIds[] = $productId->getValue(); 348 } 349 } 350 351 if (empty($failedIds)) { 352 return; 353 } 354 355 throw new CannotBulkDeleteProductException( 356 $failedIds, 357 sprintf('Failed to delete following products: "%s"', implode(', ', $failedIds)) 358 ); 359 } 360} 361