Skip to content

Commit 55f4482

Browse files
committed
fix(api): fix the search endpoint types
1 parent 9cc1e0c commit 55f4482

3 files changed

Lines changed: 150 additions & 8 deletions

File tree

config/admin/services.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,10 @@ services:
3636
PrestaShop\Module\APIResources\ApiPlatform\Normalizer\CountryIdNormalizer:
3737
tags:
3838
- { name: 'serializer.normalizer', priority: 100 }
39+
40+
PrestaShopBundle\ApiPlatform\Serializer\CQRSApiSerializer:
41+
class: PrestaShop\Module\APIResources\Serializer\QueryParameterTypeCastSerializer
42+
decorates: 'api_platform.serializer'
43+
autowire: true
44+
arguments:
45+
$decorated: '@.inner'

src/ApiPlatform/Resources/Product/FoundProduct.php

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,50 +24,83 @@
2424

2525
use ApiPlatform\Metadata\ApiProperty;
2626
use ApiPlatform\Metadata\ApiResource;
27+
use ApiPlatform\Metadata\QueryParameter;
2728
use PrestaShop\PrestaShop\Core\Domain\Product\Query\SearchProducts;
2829
use PrestaShopBundle\ApiPlatform\Metadata\CQRSGetCollection;
2930

3031
#[ApiResource(
3132
operations: [
3233
new CQRSGetCollection(
33-
uriTemplate: '/products/search/{phrase}/{resultsLimit}/{isoCode}',
34+
uriTemplate: '/products/search',
35+
scopes: [
36+
'product_read',
37+
],
38+
CQRSQuery: SearchProducts::class,
39+
parameters: [
40+
new QueryParameter(
41+
key: 'phrase',
42+
required: true,
43+
description: 'Search phrase to find products'
44+
),
45+
new QueryParameter(
46+
key: 'resultsLimit',
47+
schema: ['type' => 'integer', 'default' => 20],
48+
required: true,
49+
description: 'Maximum number of results to return'
50+
),
51+
new QueryParameter(
52+
key: 'isoCode',
53+
required: true,
54+
description: 'Currency ISO code (e.g., EUR, USD)'
55+
),
56+
new QueryParameter(
57+
key: 'orderId',
58+
schema: ['type' => 'integer'],
59+
required: false,
60+
description: 'Optional order ID for context-specific pricing'
61+
),
62+
],
3463
openapiContext: [
3564
'parameters' => [
3665
[
3766
'name' => 'phrase',
38-
'in' => 'path',
67+
'in' => 'query',
3968
'required' => true,
4069
'schema' => [
4170
'type' => 'string',
4271
],
72+
'description' => 'Search phrase to find products',
4373
],
4474
[
4575
'name' => 'resultsLimit',
46-
'in' => 'path',
76+
'in' => 'query',
4777
'required' => true,
4878
'schema' => [
49-
'type' => 'int',
79+
'type' => 'integer',
80+
'default' => 20,
5081
],
82+
'description' => 'Maximum number of results to return',
5183
],
5284
[
5385
'name' => 'isoCode',
54-
'in' => 'path',
86+
'in' => 'query',
5587
'required' => true,
5688
'schema' => [
5789
'type' => 'string',
5890
],
91+
'description' => 'Currency ISO code (e.g., EUR, USD)',
5992
],
6093
[
6194
'name' => 'orderId',
6295
'in' => 'query',
6396
'required' => false,
6497
'schema' => [
65-
'type' => 'int',
98+
'type' => 'integer',
6699
],
100+
'description' => 'Optional order ID for context-specific pricing',
67101
],
68102
],
69-
],
70-
CQRSQuery: SearchProducts::class
103+
]
71104
),
72105
],
73106
)]
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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 Academic Free License version 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/AFL-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+
* @author PrestaShop SA and Contributors <contact@prestashop.com>
17+
* @copyright Since 2007 PrestaShop SA and Contributors
18+
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
namespace PrestaShop\Module\APIResources\Serializer;
24+
25+
use PrestaShopBundle\ApiPlatform\ContextParametersProvider;
26+
use PrestaShopBundle\ApiPlatform\LocalizedValueUpdater;
27+
use PrestaShopBundle\ApiPlatform\NormalizationMapper;
28+
use PrestaShopBundle\ApiPlatform\Serializer\CQRSApiSerializer;
29+
use ReflectionClass;
30+
use ReflectionException;
31+
use ReflectionNamedType;
32+
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
33+
use Symfony\Component\Serializer\Serializer;
34+
35+
/**
36+
* Extends CQRSApiSerializer to add automatic type casting for query parameters.
37+
*/
38+
class QueryParameterTypeCastSerializer extends CQRSApiSerializer
39+
{
40+
public function __construct(
41+
Serializer $decorated,
42+
ContextParametersProvider $contextParametersProvider,
43+
ClassMetadataFactoryInterface $classMetadataFactory,
44+
LocalizedValueUpdater $localizedValueUpdater,
45+
NormalizationMapper $normalizationMapper,
46+
) {
47+
parent::__construct($decorated, $contextParametersProvider, $classMetadataFactory, $localizedValueUpdater, $normalizationMapper);
48+
}
49+
50+
/**
51+
* {@inheritdoc}
52+
*/
53+
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
54+
{
55+
if (is_array($data) && str_starts_with($type, 'PrestaShop\\PrestaShop\\Core\\Domain\\')) {
56+
$data = $this->castQueryParametersToExpectedTypes($data, $type);
57+
}
58+
59+
return parent::denormalize($data, $type, $format, $context);
60+
}
61+
62+
/**
63+
* Cast string query parameter values to their expected types based on the CQRS query constructor.
64+
*/
65+
private function castQueryParametersToExpectedTypes(array $data, string $queryClass): array
66+
{
67+
try {
68+
$reflection = new ReflectionClass($queryClass);
69+
$constructor = $reflection->getConstructor();
70+
71+
if (!$constructor) {
72+
return $data;
73+
}
74+
75+
foreach ($constructor->getParameters() as $parameter) {
76+
$paramName = $parameter->getName();
77+
78+
if (!array_key_exists($paramName, $data) || !is_string($data[$paramName])) {
79+
continue;
80+
}
81+
82+
$type = $parameter->getType();
83+
84+
if (!$type instanceof ReflectionNamedType || !$type->isBuiltin()) {
85+
continue;
86+
}
87+
88+
$data[$paramName] = match ($type->getName()) {
89+
'int' => (int) $data[$paramName],
90+
'float' => (float) $data[$paramName],
91+
'bool' => filter_var($data[$paramName], FILTER_VALIDATE_BOOLEAN),
92+
default => $data[$paramName],
93+
};
94+
}
95+
} catch (ReflectionException $e) {
96+
return $data;
97+
}
98+
99+
return $data;
100+
}
101+
}
102+

0 commit comments

Comments
 (0)