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