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\QueryHandler; 30 31use Address; 32use Configuration; 33use Currency; 34use Order; 35use PrestaShop\PrestaShop\Adapter\ContextStateManager; 36use PrestaShop\PrestaShop\Adapter\Currency\CurrencyDataProvider; 37use PrestaShop\PrestaShop\Adapter\Order\AbstractOrderHandler; 38use PrestaShop\PrestaShop\Adapter\Tools; 39use PrestaShop\PrestaShop\Core\Domain\Product\Query\SearchProducts; 40use PrestaShop\PrestaShop\Core\Domain\Product\QueryHandler\SearchProductsHandlerInterface; 41use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\FoundProduct; 42use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductCombination; 43use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductCustomizationField; 44use PrestaShop\PrestaShop\Core\Localization\CLDR\ComputingPrecision; 45use PrestaShop\PrestaShop\Core\Localization\LocaleInterface; 46use Product; 47use Shop; 48 49/** 50 * Handles products search using legacy object model 51 */ 52final class SearchProductsHandler extends AbstractOrderHandler implements SearchProductsHandlerInterface 53{ 54 /** 55 * @var int 56 */ 57 private $contextLangId; 58 59 /** 60 * @var LocaleInterface 61 */ 62 private $contextLocale; 63 64 /** 65 * @var ContextStateManager 66 */ 67 private $contextStateManager; 68 69 /** 70 * @var CurrencyDataProvider 71 */ 72 private $currencyDataProvider; 73 74 /** 75 * @var Tools 76 */ 77 private $tools; 78 79 /** 80 * @param int $contextLangId 81 * @param LocaleInterface $contextLocale 82 * @param Tools $tools 83 * @param CurrencyDataProvider $currencyDataProvider 84 * @param ContextStateManager $contextStateManager 85 */ 86 public function __construct( 87 int $contextLangId, 88 LocaleInterface $contextLocale, 89 Tools $tools, 90 CurrencyDataProvider $currencyDataProvider, 91 ContextStateManager $contextStateManager 92 ) { 93 $this->contextLangId = $contextLangId; 94 $this->contextLocale = $contextLocale; 95 $this->currencyDataProvider = $currencyDataProvider; 96 $this->tools = $tools; 97 $this->contextStateManager = $contextStateManager; 98 } 99 100 /** 101 * {@inheritdoc} 102 * 103 * @param SearchProducts $query 104 * 105 * @return array 106 */ 107 public function handle(SearchProducts $query): array 108 { 109 $currency = $this->currencyDataProvider->getCurrencyByIsoCode($query->getAlphaIsoCode()->getValue()); 110 $this->contextStateManager 111 ->setCurrency($currency) 112 ; 113 if (null !== $query->getOrderId()) { 114 $order = $this->getOrder($query->getOrderId()); 115 $this->contextStateManager 116 ->setShop(new Shop($order->id_shop)) 117 ; 118 } 119 120 try { 121 $foundProducts = $this->searchProducts($query, $currency); 122 } finally { 123 $this->contextStateManager->restorePreviousContext(); 124 } 125 126 return $foundProducts; 127 } 128 129 /** 130 * @param SearchProducts $query 131 * @param Currency $currency 132 * 133 * @return array 134 */ 135 private function searchProducts(SearchProducts $query, Currency $currency): array 136 { 137 $computingPrecision = new ComputingPrecision(); 138 $currencyPrecision = $computingPrecision->getPrecision((int) $currency->precision); 139 140 $order = $address = null; 141 if (null !== $query->getOrderId()) { 142 $order = $this->getOrder($query->getOrderId()); 143 $orderAddressId = $order->{Configuration::get('PS_TAX_ADDRESS_TYPE', null, null, $order->id_shop)}; 144 $address = new Address($orderAddressId); 145 } 146 147 $products = Product::searchByName( 148 $this->contextLangId, 149 $query->getPhrase(), 150 null, 151 $query->getResultsLimit() 152 ); 153 154 $foundProducts = []; 155 if ($products) { 156 foreach ($products as $product) { 157 $foundProduct = $this->createFoundProductFromLegacy( 158 new Product($product['id_product']), 159 $query->getAlphaIsoCode()->getValue(), 160 $currencyPrecision, 161 $order, 162 $address 163 ); 164 $foundProducts[] = $foundProduct; 165 } 166 } 167 168 return $foundProducts; 169 } 170 171 /** 172 * @param Product $product 173 * @param string $isoCodeCurrency 174 * @param int $computingPrecision 175 * @param Order|null $order 176 * @param Address|null $address 177 * 178 * @return FoundProduct 179 */ 180 private function createFoundProductFromLegacy( 181 Product $product, 182 string $isoCodeCurrency, 183 int $computingPrecision, 184 ?Order $order = null, 185 ?Address $address = null 186 ): FoundProduct { 187 // It's important to use null (not 0) as attribute ID so that Product::priceCalculation can fallback to default combination 188 $priceTaxExcluded = $this->getProductPriceForOrder((int) $product->id, null, false, $computingPrecision, $order) ?? 0.00; 189 $priceTaxIncluded = $this->getProductPriceForOrder((int) $product->id, null, true, $computingPrecision, $order) ?? 0.00; 190 $product->loadStockData(); 191 192 return new FoundProduct( 193 $product->id, 194 $product->name[$this->contextLangId], 195 $this->contextLocale->formatPrice($priceTaxExcluded, $isoCodeCurrency), 196 $this->tools->round($priceTaxIncluded, $computingPrecision), 197 $this->tools->round($priceTaxExcluded, $computingPrecision), 198 $product->getTaxesRate($address), 199 Product::getQuantity($product->id), 200 $product->location, 201 (bool) Product::isAvailableWhenOutOfStock($product->out_of_stock), 202 $this->getProductCombinations($product, $isoCodeCurrency, $computingPrecision, $order), 203 $this->getProductCustomizationFields($product) 204 ); 205 } 206 207 /** 208 * @param Product $product 209 * 210 * @return ProductCustomizationField[] 211 */ 212 private function getProductCustomizationFields(Product $product): array 213 { 214 $fields = $product->getCustomizationFields(); 215 $customizationFields = []; 216 217 if (false !== $fields) { 218 foreach ($fields as $typeId => $typeFields) { 219 foreach ($typeFields as $field) { 220 $customizationField = new ProductCustomizationField( 221 (int) $field[$this->contextLangId]['id_customization_field'], 222 (int) $typeId, 223 $field[$this->contextLangId]['name'], 224 (bool) $field[$this->contextLangId]['required'] 225 ); 226 227 $customizationFields[$customizationField->getCustomizationFieldId()] = $customizationField; 228 } 229 } 230 } 231 232 return $customizationFields; 233 } 234 235 /** 236 * @param Product $product 237 * @param string $currencyIsoCode 238 * @param int $computingPrecision 239 * @param Order|null $order 240 * 241 * @return array 242 */ 243 private function getProductCombinations( 244 Product $product, 245 string $currencyIsoCode, 246 int $computingPrecision, 247 ?Order $order = null 248 ): array { 249 $productCombinations = []; 250 $combinations = $product->getAttributeCombinations(); 251 252 if (false !== $combinations) { 253 foreach ($combinations as $combination) { 254 $productAttributeId = (int) $combination['id_product_attribute']; 255 $attribute = $combination['attribute_name']; 256 257 if (isset($productCombinations[$productAttributeId])) { 258 $existingAttribute = $productCombinations[$productAttributeId]->getAttribute(); 259 $attribute = $existingAttribute . ' - ' . $attribute; 260 } 261 262 $priceTaxExcluded = $this->getProductPriceForOrder((int) $product->id, $productAttributeId, false, $computingPrecision, $order); 263 $priceTaxIncluded = $this->getProductPriceForOrder((int) $product->id, $productAttributeId, true, $computingPrecision, $order); 264 265 $productCombination = new ProductCombination( 266 $productAttributeId, 267 $attribute, 268 $combination['quantity'], 269 $this->contextLocale->formatPrice($priceTaxExcluded, $currencyIsoCode), 270 $priceTaxExcluded, 271 $priceTaxIncluded, 272 $combination['location'], 273 $combination['reference'] 274 ); 275 276 $productCombinations[$productCombination->getAttributeCombinationId()] = $productCombination; 277 } 278 } 279 280 return $productCombinations; 281 } 282 283 /** 284 * @param int $productId 285 * @param int|null $productAttributeId 286 * @param bool $withTaxes 287 * @param int $computingPrecision 288 * @param Order|null $order 289 * 290 * @return float 291 */ 292 private function getProductPriceForOrder( 293 int $productId, 294 ?int $productAttributeId, 295 bool $withTaxes, 296 int $computingPrecision, 297 ?Order $order) 298 { 299 if (null === $order) { 300 return Product::getPriceStatic($productId, $withTaxes, $productAttributeId, $computingPrecision); 301 } 302 303 return Product::getPriceStatic( 304 $productId, 305 $withTaxes, 306 $productAttributeId, 307 $computingPrecision, 308 null, 309 false, 310 true, 311 1, 312 false, 313 $order->id_customer, 314 $order->id_cart, 315 $order->{Configuration::get('PS_TAX_ADDRESS_TYPE', null, null, $order->id_shop)} 316 ); 317 } 318} 319