diff --git a/src/Laravel/ApiPlatformProvider.php b/src/Laravel/ApiPlatformProvider.php index 264d2f480f6..b52fef2d4ee 100644 --- a/src/Laravel/ApiPlatformProvider.php +++ b/src/Laravel/ApiPlatformProvider.php @@ -211,7 +211,10 @@ public function register(): void ); }); - $this->app->singleton(ModelMetadata::class); + $this->app->singleton(ModelMetadata::class, function () { + return new ModelMetadata(); + }); + $this->app->bind(LoaderInterface::class, AttributeLoader::class); $this->app->bind(ClassMetadataFactoryInterface::class, ClassMetadataFactory::class); $this->app->singleton(ClassMetadataFactory::class, function (Application $app) { @@ -313,9 +316,14 @@ public function register(): void $this->app->bind(NameConverterInterface::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); + } + $defaultContext = $config->get('api-platform.serializer', []); - return new HydraPrefixNameConverter(new MetadataAwareNameConverter($app->make(ClassMetadataFactoryInterface::class), $app->make(SnakeCaseToCamelCaseNameConverter::class)), $defaultContext); + return new HydraPrefixNameConverter(new MetadataAwareNameConverter($app->make(ClassMetadataFactoryInterface::class), $nameConverter), $defaultContext); }); $this->app->singleton(OperationMetadataFactory::class, function (Application $app) { diff --git a/src/Laravel/Eloquent/Metadata/ModelMetadata.php b/src/Laravel/Eloquent/Metadata/ModelMetadata.php index 4259017b2d3..bd89142d0af 100644 --- a/src/Laravel/Eloquent/Metadata/ModelMetadata.php +++ b/src/Laravel/Eloquent/Metadata/ModelMetadata.php @@ -13,11 +13,12 @@ namespace ApiPlatform\Laravel\Eloquent\Metadata; -use ApiPlatform\Metadata\Util\CamelCaseToSnakeCaseNameConverter; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Collection; use Illuminate\Support\Str; +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** * Inspired from Illuminate\Database\Console\ShowModelCommand. @@ -26,8 +27,6 @@ */ final class ModelMetadata { - private CamelCaseToSnakeCaseNameConverter $relationNameConverter; - /** * @var array> */ @@ -57,9 +56,8 @@ final class ModelMetadata 'morphedByMany', ]; - public function __construct() + public function __construct(private NameConverterInterface $relationNameConverter = new CamelCaseToSnakeCaseNameConverter()) { - $this->relationNameConverter = new CamelCaseToSnakeCaseNameConverter(); } /** diff --git a/src/Laravel/Tests/SnakeCaseApiTest.php b/src/Laravel/Tests/SnakeCaseApiTest.php new file mode 100644 index 00000000000..f9d0f9b6fb1 --- /dev/null +++ b/src/Laravel/Tests/SnakeCaseApiTest.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Str; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; + +class SnakeCaseApiTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.name_converter', null); + $config->set('api-platform.formats', ['jsonld' => ['application/ld+json']]); + $config->set('api-platform.docs_formats', ['jsonld' => ['application/ld+json']]); + }); + } + + public function testRelationIsHandledOnCreateWithNestedDataSnakeCase(): void + { + $cartData = [ + 'product_sku' => 'SKU_TEST_001', + 'quantity' => 2, + 'price_at_addition' => '19.99', + 'shopping_cart' => [ + 'user_identifier' => 'user-'.Str::uuid()->toString(), + 'status' => 'active', + ], + ]; + + $response = $this->postJson('/api/cart_items', $cartData, ['accept' => 'application/ld+json', 'content-type' => 'application/ld+json']); + $response->assertStatus(201); + + $response + ->assertJson([ + '@context' => '/api/contexts/CartItem', + '@id' => '/api/cart_items/1', + '@type' => 'CartItem', + 'id' => 1, + 'product_sku' => 'SKU_TEST_001', + 'quantity' => 2, + 'price_at_addition' => 19.99, + 'shopping_cart' => [ + '@id' => '/api/shopping_carts/1', + '@type' => 'ShoppingCart', + 'user_identifier' => $cartData['shopping_cart']['user_identifier'], + 'status' => 'active', + ], + ]); + } +} diff --git a/src/Laravel/config/api-platform.php b/src/Laravel/config/api-platform.php index 5c5c18c968a..84486479490 100644 --- a/src/Laravel/config/api-platform.php +++ b/src/Laravel/config/api-platform.php @@ -11,6 +11,7 @@ declare(strict_types=1); +use ApiPlatform\Laravel\Eloquent\Serializer\SnakeCaseToCamelCaseNameConverter; use ApiPlatform\Metadata\UrlGeneratorInterface; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\AuthenticationException; @@ -86,6 +87,8 @@ // 'middleware' => null ], + 'name_converter' => SnakeCaseToCamelCaseNameConverter::class, + 'exception_to_status' => [ AuthenticationException::class => 401, AuthorizationException::class => 403, diff --git a/src/Metadata/Property/Factory/ClassLevelAttributePropertyNameCollectionFactory.php b/src/Metadata/Property/Factory/ClassLevelAttributePropertyNameCollectionFactory.php index 63283013f16..06df512551f 100644 --- a/src/Metadata/Property/Factory/ClassLevelAttributePropertyNameCollectionFactory.php +++ b/src/Metadata/Property/Factory/ClassLevelAttributePropertyNameCollectionFactory.php @@ -33,17 +33,17 @@ public function create(string $resourceClass, array $options = []): PropertyName return $parentPropertyNameCollection ?? new PropertyNameCollection(); } - $properties = $parentPropertyNameCollection ? iterator_to_array($parentPropertyNameCollection) : []; + $properties = $parentPropertyNameCollection ? array_flip(iterator_to_array($parentPropertyNameCollection)) : []; $refl = new \ReflectionClass($resourceClass); $attributes = $refl->getAttributes(ApiProperty::class); foreach ($attributes as $attribute) { $instance = $attribute->newInstance(); if ($property = $instance->getProperty()) { - $properties[] = $property; + $properties[$property] = true; } } - return new PropertyNameCollection($properties); + return new PropertyNameCollection(array_keys($properties)); } }