Skip to content

Commit 730d17a

Browse files
committed
fix(laravel): validate the model instead of body
1 parent 4e7e861 commit 730d17a

File tree

6 files changed

+69
-15
lines changed

6 files changed

+69
-15
lines changed

src/Laravel/ApiPlatformDeferredProvider.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
use ApiPlatform\Laravel\Metadata\ParameterValidationResourceMetadataCollectionFactory;
4747
use ApiPlatform\Laravel\State\ParameterValidatorProvider;
4848
use ApiPlatform\Laravel\State\SwaggerUiProcessor;
49+
use ApiPlatform\Laravel\State\ValidateProvider;
4950
use ApiPlatform\Metadata\InflectorInterface;
5051
use ApiPlatform\Metadata\Operation\PathSegmentNameGeneratorInterface;
5152
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
@@ -76,7 +77,6 @@
7677
use ApiPlatform\State\Pagination\Pagination;
7778
use ApiPlatform\State\ParameterProviderInterface;
7879
use ApiPlatform\State\ProcessorInterface;
79-
use ApiPlatform\State\Provider\DeserializeProvider;
8080
use ApiPlatform\State\Provider\ParameterProvider;
8181
use ApiPlatform\State\Provider\SecurityParameterProvider;
8282
use ApiPlatform\State\ProviderInterface;
@@ -133,7 +133,7 @@ public function register(): void
133133
return new ParameterProvider(
134134
new ParameterValidatorProvider(
135135
new SecurityParameterProvider(
136-
$app->make(DeserializeProvider::class),
136+
$app->make(ValidateProvider::class),
137137
$app->make(ResourceAccessCheckerInterface::class)
138138
),
139139
),

src/Laravel/ApiPlatformProvider.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -343,12 +343,12 @@ public function register(): void
343343
return new SwaggerUiProvider($app->make(ReadProvider::class), $app->make(OpenApiFactoryInterface::class), $config->get('api-platform.swagger_ui.enabled', false));
344344
});
345345

346-
$this->app->singleton(ValidateProvider::class, function (Application $app) {
347-
return new ValidateProvider($app->make(SwaggerUiProvider::class), $app);
346+
$this->app->singleton(DeserializeProvider::class, function (Application $app) {
347+
return new DeserializeProvider($app->make(SwaggerUiProvider::class), $app->make(SerializerInterface::class), $app->make(SerializerContextBuilderInterface::class));
348348
});
349349

350-
$this->app->singleton(DeserializeProvider::class, function (Application $app) {
351-
return new DeserializeProvider($app->make(ValidateProvider::class), $app->make(SerializerInterface::class), $app->make(SerializerContextBuilderInterface::class));
350+
$this->app->singleton(ValidateProvider::class, function (Application $app) {
351+
return new ValidateProvider($app->make(DeserializeProvider::class), $app, $app->make(ObjectNormalizer::class));
352352
});
353353

354354
if (class_exists(JsonApiProvider::class)) {

src/Laravel/State/ValidateProvider.php

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@
1414
namespace ApiPlatform\Laravel\State;
1515

1616
use ApiPlatform\Metadata\Error;
17+
use ApiPlatform\Metadata\Exception\RuntimeException;
1718
use ApiPlatform\Metadata\Operation;
1819
use ApiPlatform\State\ProviderInterface;
1920
use Illuminate\Contracts\Foundation\Application;
21+
use Illuminate\Database\Eloquent\Model;
2022
use Illuminate\Foundation\Http\FormRequest;
2123
use Illuminate\Support\Facades\Validator;
2224
use Illuminate\Validation\ValidationException;
25+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
2326

2427
/**
2528
* @implements ProviderInterface<object>
@@ -34,12 +37,13 @@ final class ValidateProvider implements ProviderInterface
3437
public function __construct(
3538
private readonly ProviderInterface $inner,
3639
private readonly Application $app,
40+
// TODO: trigger deprecation in API Platform 4.2 when this is not defined
41+
private readonly ?NormalizerInterface $normalizer = null,
3742
) {
3843
}
3944

4045
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
4146
{
42-
$request = $context['request'];
4347
$body = $this->inner->provide($operation, $uriVariables, $context);
4448

4549
if ($operation instanceof Error) {
@@ -74,12 +78,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
7478
return $body;
7579
}
7680

77-
// In Symfony, validation is done on the Resource object (here $body) using Deserialization before Validation
78-
// Here, we did not deserialize yet, we validate on the raw body before.
79-
$validationBody = $request->request->all();
80-
if ('jsonapi' === $request->getRequestFormat()) {
81-
$validationBody = $validationBody['data']['attributes'];
82-
}
81+
$validationBody = $this->getBodyForValidation($body);
8382

8483
$validator = Validator::make($validationBody, $rules);
8584
if ($validator->fails()) {
@@ -88,4 +87,35 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
8887

8988
return $body;
9089
}
90+
91+
/**
92+
* @return array<string, mixed>
93+
*/
94+
private function getBodyForValidation(mixed $body): array
95+
{
96+
if (!$body) {
97+
return [];
98+
}
99+
100+
if ($body instanceof Model) {
101+
return $body->toArray();
102+
}
103+
104+
if ($this->normalizer) {
105+
if (!\is_array($v = $this->normalizer->normalize($body))) {
106+
throw new RuntimeException('An array is expected.');
107+
}
108+
109+
return $v;
110+
}
111+
112+
// hopefully this path never gets used, its there for BC-layer only
113+
// TODO: deprecation in API Platform 4.2
114+
// TODO: remove in 5.0
115+
if ($s = json_encode($body)) {
116+
return json_decode($s, true);
117+
}
118+
119+
throw new RuntimeException('Could not transform the denormalized body in an array for validation');
120+
}
91121
}

src/Laravel/Tests/SnakeCaseApiTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,20 @@ public function testRelationIsHandledOnCreateWithNestedDataSnakeCase(): void
6969
],
7070
]);
7171
}
72+
73+
public function testFailWithCamelCase(): void
74+
{
75+
$cartData = [
76+
'productSku' => 'SKU_TEST_001',
77+
'quantity' => 2,
78+
'priceAtAddition' => '19.99',
79+
'shoppingCart' => [
80+
'userIdentifier' => 'user-'.Str::uuid()->toString(),
81+
'status' => 'active',
82+
],
83+
];
84+
85+
$response = $this->postJson('/api/cart_items', $cartData, ['accept' => 'application/ld+json', 'content-type' => 'application/ld+json']);
86+
$response->assertStatus(422);
87+
}
7288
}

src/Laravel/workbench/app/ApiResource/RuleValidation.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
)]
2424
class RuleValidation
2525
{
26-
public function __construct(public int $prop, public ?int $max = null)
26+
public function __construct(public ?int $prop = null, public ?int $max = null)
2727
{
2828
}
2929
}

src/Laravel/workbench/app/Models/CartItem.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,15 @@
2020
use Illuminate\Database\Eloquent\Relations\BelongsTo;
2121
use Symfony\Component\Serializer\Attribute\Groups;
2222

23-
#[ApiResource(denormalizationContext: ['groups' => ['cart_item.write']], normalizationContext: ['groups' => ['cart_item.write']])]
23+
#[ApiResource(
24+
denormalizationContext: ['groups' => ['cart_item.write']],
25+
normalizationContext: ['groups' => ['cart_item.write']],
26+
rules: [
27+
'product_sku' => 'required',
28+
'quantity' => 'required',
29+
'price_at_addition' => 'required',
30+
]
31+
)]
2432
#[Groups('cart_item.write')]
2533
#[ApiProperty(serialize: new Groups(['shopping_cart.write']), property: 'product_sku')]
2634
#[ApiProperty(serialize: new Groups(['shopping_cart.write']), property: 'price_at_addition')]

0 commit comments

Comments
 (0)