Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Laravel/ApiPlatformDeferredProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
use ApiPlatform\Laravel\Metadata\ParameterValidationResourceMetadataCollectionFactory;
use ApiPlatform\Laravel\State\ParameterValidatorProvider;
use ApiPlatform\Laravel\State\SwaggerUiProcessor;
use ApiPlatform\Laravel\State\ValidateProvider;
use ApiPlatform\Metadata\InflectorInterface;
use ApiPlatform\Metadata\Operation\PathSegmentNameGeneratorInterface;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
Expand Down Expand Up @@ -76,7 +77,6 @@
use ApiPlatform\State\Pagination\Pagination;
use ApiPlatform\State\ParameterProviderInterface;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\State\Provider\DeserializeProvider;
use ApiPlatform\State\Provider\ParameterProvider;
use ApiPlatform\State\Provider\SecurityParameterProvider;
use ApiPlatform\State\ProviderInterface;
Expand Down Expand Up @@ -133,7 +133,7 @@
return new ParameterProvider(
new ParameterValidatorProvider(
new SecurityParameterProvider(
$app->make(DeserializeProvider::class),
$app->make(ValidateProvider::class),

Check warning on line 136 in src/Laravel/ApiPlatformDeferredProvider.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/ApiPlatformDeferredProvider.php#L136

Added line #L136 was not covered by tests
$app->make(ResourceAccessCheckerInterface::class)
),
),
Expand Down
16 changes: 11 additions & 5 deletions src/Laravel/ApiPlatformProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@
use ApiPlatform\Laravel\Eloquent\Metadata\ResourceClassResolver as EloquentResourceClassResolver;
use ApiPlatform\Laravel\Eloquent\PropertyAccess\PropertyAccessor as EloquentPropertyAccessor;
use ApiPlatform\Laravel\Eloquent\Serializer\SerializerContextBuilder as EloquentSerializerContextBuilder;
use ApiPlatform\Laravel\Eloquent\Serializer\SnakeCaseToCamelCaseNameConverter;
use ApiPlatform\Laravel\Exception\ErrorHandler;
use ApiPlatform\Laravel\GraphQl\Controller\EntrypointController as GraphQlEntrypointController;
use ApiPlatform\Laravel\GraphQl\Controller\GraphiQlController;
Expand Down Expand Up @@ -178,6 +177,7 @@
use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface;
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer;
use Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer;
Expand Down Expand Up @@ -343,12 +343,18 @@
return new SwaggerUiProvider($app->make(ReadProvider::class), $app->make(OpenApiFactoryInterface::class), $config->get('api-platform.swagger_ui.enabled', false));
});

$this->app->singleton(ValidateProvider::class, function (Application $app) {
return new ValidateProvider($app->make(SwaggerUiProvider::class), $app);
$this->app->singleton(DeserializeProvider::class, function (Application $app) {
return new DeserializeProvider($app->make(SwaggerUiProvider::class), $app->make(SerializerInterface::class), $app->make(SerializerContextBuilderInterface::class));

Check warning on line 347 in src/Laravel/ApiPlatformProvider.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/ApiPlatformProvider.php#L346-L347

Added lines #L346 - L347 were not covered by tests
});

$this->app->singleton(DeserializeProvider::class, function (Application $app) {
return new DeserializeProvider($app->make(ValidateProvider::class), $app->make(SerializerInterface::class), $app->make(SerializerContextBuilderInterface::class));
$this->app->singleton(ValidateProvider::class, function (Application $app) {
$config = $app['config'];
$nameConverter = $config->get('api-platform.name_converter', SnakeCaseToCamelCaseNameConverter::class);
if ($nameConverter && class_exists($nameConverter)) {
$nameConverter = $app->make($nameConverter);

Check warning on line 354 in src/Laravel/ApiPlatformProvider.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/ApiPlatformProvider.php#L350-L354

Added lines #L350 - L354 were not covered by tests
}

return new ValidateProvider($app->make(DeserializeProvider::class), $app, $app->make(ObjectNormalizer::class), $nameConverter);

Check warning on line 357 in src/Laravel/ApiPlatformProvider.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/ApiPlatformProvider.php#L357

Added line #L357 was not covered by tests
});

if (class_exists(JsonApiProvider::class)) {
Expand Down

This file was deleted.

47 changes: 40 additions & 7 deletions src/Laravel/State/ValidateProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@
namespace ApiPlatform\Laravel\State;

use ApiPlatform\Metadata\Error;
use ApiPlatform\Metadata\Exception\RuntimeException;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

/**
* @implements ProviderInterface<object>
Expand All @@ -34,12 +38,15 @@
public function __construct(
private readonly ProviderInterface $inner,
private readonly Application $app,
// TODO: trigger deprecation in API Platform 4.2 when this is not defined
private readonly ?NormalizerInterface $normalizer = null,
?NameConverterInterface $nameConverter = null,
) {
$this->nameConverter = $nameConverter;

Check warning on line 45 in src/Laravel/State/ValidateProvider.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/State/ValidateProvider.php#L45

Added line #L45 was not covered by tests
}

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

if ($operation instanceof Error) {
Expand Down Expand Up @@ -74,12 +81,7 @@
return $body;
}

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

Check warning on line 84 in src/Laravel/State/ValidateProvider.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/State/ValidateProvider.php#L84

Added line #L84 was not covered by tests

$validator = Validator::make($validationBody, $rules);
if ($validator->fails()) {
Expand All @@ -88,4 +90,35 @@

return $body;
}

/**
* @return array<string, mixed>
*/
private function getBodyForValidation(mixed $body): array

Check warning on line 97 in src/Laravel/State/ValidateProvider.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/State/ValidateProvider.php#L97

Added line #L97 was not covered by tests
{
if (!$body) {
return [];

Check warning on line 100 in src/Laravel/State/ValidateProvider.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/State/ValidateProvider.php#L99-L100

Added lines #L99 - L100 were not covered by tests
}

if ($body instanceof Model) {
return $body->toArray();

Check warning on line 104 in src/Laravel/State/ValidateProvider.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/State/ValidateProvider.php#L103-L104

Added lines #L103 - L104 were not covered by tests
}

if ($this->normalizer) {
if (!\is_array($v = $this->normalizer->normalize($body))) {
throw new RuntimeException('An array is expected.');

Check warning on line 109 in src/Laravel/State/ValidateProvider.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/State/ValidateProvider.php#L107-L109

Added lines #L107 - L109 were not covered by tests
}

return $v;

Check warning on line 112 in src/Laravel/State/ValidateProvider.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/State/ValidateProvider.php#L112

Added line #L112 was not covered by tests
}

// hopefully this path never gets used, its there for BC-layer only
// TODO: deprecation in API Platform 4.2
// TODO: remove in 5.0
if ($s = json_encode($body)) {
return json_decode($s, true);

Check warning on line 119 in src/Laravel/State/ValidateProvider.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/State/ValidateProvider.php#L118-L119

Added lines #L118 - L119 were not covered by tests
}

throw new RuntimeException('Could not transform the denormalized body in an array for validation');

Check warning on line 122 in src/Laravel/State/ValidateProvider.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/State/ValidateProvider.php#L122

Added line #L122 was not covered by tests
}
}
5 changes: 4 additions & 1 deletion src/Laravel/State/ValidationErrorTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@
use ApiPlatform\Laravel\ApiResource\ValidationError;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Validation\ValidationException;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;

trait ValidationErrorTrait
{
private ?NameConverterInterface $nameConverter = null;

private function getValidationError(Validator $validator, ValidationException $e): ValidationError
{
$errors = $validator->errors();
$violations = [];
$id = hash('xxh3', implode(',', $errors->keys()));
foreach ($errors->messages() as $prop => $message) {
$violations[] = ['propertyPath' => $prop, 'message' => implode(\PHP_EOL, $message)];
$violations[] = ['propertyPath' => $this->nameConverter ? $this->nameConverter->normalize($prop) : $prop, 'message' => implode(\PHP_EOL, $message)];

Check warning on line 31 in src/Laravel/State/ValidationErrorTrait.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/State/ValidationErrorTrait.php#L31

Added line #L31 was not covered by tests
}

return new ValidationError($e->getMessage(), $id, $e, $violations);
Expand Down
16 changes: 16 additions & 0 deletions src/Laravel/Tests/SnakeCaseApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,20 @@
],
]);
}

public function testFailWithCamelCase(): void

Check warning on line 73 in src/Laravel/Tests/SnakeCaseApiTest.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/Tests/SnakeCaseApiTest.php#L73

Added line #L73 was not covered by tests
{
$cartData = [
'productSku' => 'SKU_TEST_001',
'quantity' => 2,
'priceAtAddition' => '19.99',
'shoppingCart' => [
'userIdentifier' => 'user-'.Str::uuid()->toString(),
'status' => 'active',
],
];

Check warning on line 83 in src/Laravel/Tests/SnakeCaseApiTest.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/Tests/SnakeCaseApiTest.php#L75-L83

Added lines #L75 - L83 were not covered by tests

$response = $this->postJson('/api/cart_items', $cartData, ['accept' => 'application/ld+json', 'content-type' => 'application/ld+json']);
$response->assertStatus(422);

Check warning on line 86 in src/Laravel/Tests/SnakeCaseApiTest.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/Tests/SnakeCaseApiTest.php#L85-L86

Added lines #L85 - L86 were not covered by tests
}
}
60 changes: 60 additions & 0 deletions src/Laravel/Tests/ValidationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Laravel\Tests;

use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Orchestra\Testbench\Concerns\WithWorkbench;
use Orchestra\Testbench\TestCase;

class ValidationTest extends TestCase
{
use ApiTestAssertionsTrait;
use RefreshDatabase;
use WithWorkbench;

/**
* @param Application $app
*/
protected function defineEnvironment($app): void

Check warning on line 32 in src/Laravel/Tests/ValidationTest.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/Tests/ValidationTest.php#L32

Added line #L32 was not covered by tests
{
tap($app['config'], function (Repository $config): void {
$config->set('api-platform.formats', ['jsonld' => ['application/ld+json']]);
$config->set('api-platform.docs_formats', ['jsonld' => ['application/ld+json']]);
});

Check warning on line 37 in src/Laravel/Tests/ValidationTest.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/Tests/ValidationTest.php#L34-L37

Added lines #L34 - L37 were not covered by tests
}

public function testValidationCamelCase(): void

Check warning on line 40 in src/Laravel/Tests/ValidationTest.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/Tests/ValidationTest.php#L40

Added line #L40 was not covered by tests
{
$data = [
'surName' => '',
];

Check warning on line 44 in src/Laravel/Tests/ValidationTest.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/Tests/ValidationTest.php#L42-L44

Added lines #L42 - L44 were not covered by tests

$response = $this->postJson('/api/issue_6932', $data, ['accept' => 'application/ld+json', 'content-type' => 'application/ld+json']);
$response->assertJsonFragment(['violations' => [['propertyPath' => 'surName', 'message' => 'The sur name field is required.']]]); // validate that the name has been converted
$response->assertStatus(422);

Check warning on line 48 in src/Laravel/Tests/ValidationTest.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/Tests/ValidationTest.php#L46-L48

Added lines #L46 - L48 were not covered by tests
}

public function testValidationSnakeCase(): void

Check warning on line 51 in src/Laravel/Tests/ValidationTest.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/Tests/ValidationTest.php#L51

Added line #L51 was not covered by tests
{
$data = [
'sur_name' => 'test',
];

Check warning on line 55 in src/Laravel/Tests/ValidationTest.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/Tests/ValidationTest.php#L53-L55

Added lines #L53 - L55 were not covered by tests

$response = $this->postJson('/api/issue_6932', $data, ['accept' => 'application/ld+json', 'content-type' => 'application/ld+json']);
$response->assertStatus(422);

Check warning on line 58 in src/Laravel/Tests/ValidationTest.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/Tests/ValidationTest.php#L57-L58

Added lines #L57 - L58 were not covered by tests
}
}
2 changes: 1 addition & 1 deletion src/Laravel/config/api-platform.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@

declare(strict_types=1);

use ApiPlatform\Laravel\Eloquent\Serializer\SnakeCaseToCamelCaseNameConverter;
use ApiPlatform\Metadata\UrlGeneratorInterface;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter;

return [
'title' => 'API Platform',
Expand Down
2 changes: 1 addition & 1 deletion src/Laravel/workbench/app/ApiResource/RuleValidation.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
)]
class RuleValidation
{
public function __construct(public int $prop, public ?int $max = null)
public function __construct(public ?int $prop = null, public ?int $max = null)

Check warning on line 26 in src/Laravel/workbench/app/ApiResource/RuleValidation.php

View check run for this annotation

Codecov / codecov/patch

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

Added line #L26 was not covered by tests
{
}
}
10 changes: 9 additions & 1 deletion src/Laravel/workbench/app/Models/CartItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,15 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Symfony\Component\Serializer\Attribute\Groups;

#[ApiResource(denormalizationContext: ['groups' => ['cart_item.write']], normalizationContext: ['groups' => ['cart_item.write']])]
#[ApiResource(
denormalizationContext: ['groups' => ['cart_item.write']],
normalizationContext: ['groups' => ['cart_item.write']],
rules: [
'product_sku' => 'required',
'quantity' => 'required',
'price_at_addition' => 'required',
]
)]

Check warning on line 31 in src/Laravel/workbench/app/Models/CartItem.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/workbench/app/Models/CartItem.php#L24-L31

Added lines #L24 - L31 were not covered by tests
#[Groups('cart_item.write')]
#[ApiProperty(serialize: new Groups(['shopping_cart.write']), property: 'product_sku')]
#[ApiProperty(serialize: new Groups(['shopping_cart.write']), property: 'price_at_addition')]
Expand Down
33 changes: 33 additions & 0 deletions src/Laravel/workbench/app/Models/Issue6932.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace App\Models;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post;
use Illuminate\Database\Eloquent\Model;

#[ApiResource(
operations: [
new Post(
uriTemplate: '/issue_6932',
rules: [
'sur_name' => 'required',
]
),
],
)]

Check warning on line 29 in src/Laravel/workbench/app/Models/Issue6932.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/workbench/app/Models/Issue6932.php#L21-L29

Added lines #L21 - L29 were not covered by tests
class Issue6932 extends Model
{
protected $table = 'issue6932';
}
Loading
Loading