Skip to content

Commit bd6a57c

Browse files
authored
fix(laravel): snake case props (#6532)
* fix(laravel): snake case props # Conflicts: # src/Laravel/Eloquent/PropertyAccess/PropertyAccessor.php * feat: convert snake case to camel case * cleanup * fix: cs * tests: add name converter test * fix: phpstan
1 parent 303ddc3 commit bd6a57c

File tree

9 files changed

+93
-17
lines changed

9 files changed

+93
-17
lines changed

src/Laravel/ApiPlatformProvider.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
use ApiPlatform\Laravel\Eloquent\Metadata\ResourceClassResolver as EloquentResourceClassResolver;
5151
use ApiPlatform\Laravel\Eloquent\PropertyAccess\PropertyAccessor as EloquentPropertyAccessor;
5252
use ApiPlatform\Laravel\Eloquent\Serializer\SerializerContextBuilder as EloquentSerializerContextBuilder;
53+
use ApiPlatform\Laravel\Eloquent\Serializer\SnakeCaseToCamelCaseNameConverter;
5354
use ApiPlatform\Laravel\Eloquent\State\CollectionProvider;
5455
use ApiPlatform\Laravel\Eloquent\State\ItemProvider;
5556
use ApiPlatform\Laravel\Eloquent\State\LinksHandler;
@@ -131,7 +132,6 @@
131132
use Negotiation\Negotiator;
132133
use phpDocumentor\Reflection\DocBlockFactory;
133134
use Psr\Log\LoggerInterface;
134-
use Symfony\Component\PropertyAccess\PropertyAccess;
135135
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
136136
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
137137
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
@@ -142,7 +142,6 @@
142142
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
143143
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
144144
use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface;
145-
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter as NameConverterCamelCaseToSnakeCaseNameConverter;
146145
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
147146
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
148147
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
@@ -284,11 +283,11 @@ public function register(): void
284283
});
285284

286285
$this->app->bind(PropertyAccessorInterface::class, function () {
287-
return new EloquentPropertyAccessor(PropertyAccess::createPropertyAccessor());
286+
return new EloquentPropertyAccessor();
288287
});
289288

290289
$this->app->bind(NameConverterInterface::class, function (Application $app) {
291-
return new MetadataAwareNameConverter($app->make(ClassMetadataFactoryInterface::class), new NameConverterCamelCaseToSnakeCaseNameConverter());
290+
return new MetadataAwareNameConverter($app->make(ClassMetadataFactoryInterface::class), $app->make(SnakeCaseToCamelCaseNameConverter::class));
292291
});
293292

294293
$this->app->bind(OperationMetadataFactoryInterface::class, OperationMetadataFactory::class);

src/Laravel/Eloquent/PropertyAccess/PropertyAccessor.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace ApiPlatform\Laravel\Eloquent\PropertyAccess;
1515

1616
use Illuminate\Database\Eloquent\Relations\HasMany;
17+
use Symfony\Component\PropertyAccess\PropertyAccess;
1718
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
1819
use Symfony\Component\PropertyAccess\PropertyPathInterface;
1920

@@ -22,9 +23,12 @@
2223
*/
2324
final class PropertyAccessor implements PropertyAccessorInterface
2425
{
26+
private readonly PropertyAccessorInterface $inner;
27+
2528
public function __construct(
26-
private readonly PropertyAccessorInterface $inner,
29+
?PropertyAccessorInterface $inner = null,
2730
) {
31+
$this->inner = $inner ?? PropertyAccess::createPropertyAccessor();
2832
}
2933

3034
/**
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Laravel\Eloquent\Serializer;
15+
16+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
17+
18+
/**
19+
* Underscore to cameCase name converter.
20+
*
21+
* @internal
22+
*
23+
* @see Adapted from https://github.com/symfony/symfony/blob/7.2/src/Symfony/Component/Serializer/NameConverter/CamelCaseToSnakeCaseNameConverter.php.
24+
*
25+
* @author Kévin Dunglas <[email protected]>
26+
* @author Aurélien Pillevesse <[email protected]>
27+
* @copyright Fabien Potencier <[email protected]>
28+
*/
29+
final class SnakeCaseToCamelCaseNameConverter implements NameConverterInterface
30+
{
31+
/**
32+
* @param string[]|null $attributes The list of attributes to rename or null for all attributes
33+
*/
34+
public function __construct(
35+
private readonly ?array $attributes = null,
36+
) {
37+
}
38+
39+
/**
40+
* @param class-string|null $class
41+
* @param array<string, mixed> $context
42+
*/
43+
public function normalize(
44+
string $propertyName, ?string $class = null, ?string $format = null, array $context = []
45+
): string {
46+
if (null === $this->attributes || \in_array($propertyName, $this->attributes, true)) {
47+
return lcfirst(preg_replace_callback(
48+
'/(^|_|\.)+(.)/',
49+
fn ($match) => ('.' === $match[1] ? '_' : '').strtoupper($match[2]),
50+
$propertyName
51+
));
52+
}
53+
54+
return $propertyName;
55+
}
56+
57+
/**
58+
* @param class-string|null $class
59+
* @param array<string, mixed> $context
60+
*/
61+
public function denormalize(
62+
string $propertyName, ?string $class = null, ?string $format = null, array $context = []
63+
): string {
64+
$snakeCased = strtolower(preg_replace('/[A-Z]/', '_\\0', lcfirst($propertyName)));
65+
if (null === $this->attributes || \in_array($snakeCased, $this->attributes, true)) {
66+
return $snakeCased;
67+
}
68+
69+
return $propertyName;
70+
}
71+
}

src/Laravel/Tests/JsonApiTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public function testCreateBook(): void
6969
'/api/books',
7070
[
7171
'data' => [
72-
'attributes' => ['name' => 'Don Quichotte', 'isbn' => fake()->isbn13()],
72+
'attributes' => ['name' => 'Don Quichotte', 'isbn' => fake()->isbn13(), 'publicationDate' => fake()->date()],
7373
'relationships' => ['author' => ['data' => ['id' => $this->getIriFromResource($author), 'type' => 'Author']]],
7474
],
7575
],

src/Laravel/Tests/JsonLdTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public function testCreateBook(): void
6363
'name' => 'Don Quichotte',
6464
'author' => $this->getIriFromResource($author),
6565
'isbn' => fake()->isbn13(),
66+
'publicationDate' => fake()->date(),
6667
],
6768
[
6869
'accept' => 'application/ld+json',

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class Book extends Model
3030
use HasFactory;
3131
use HasUlids;
3232

33-
protected $visible = ['name', 'author', 'isbn'];
33+
protected $visible = ['name', 'author', 'isbn', 'publication_date'];
3434
protected $fillable = ['name'];
3535

3636
public function author(): BelongsTo

src/Laravel/workbench/database/factories/BookFactory.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public function definition(): array
3636
'id' => (string) new Ulid(),
3737
'author_id' => AuthorFactory::new(),
3838
'isbn' => fake()->isbn13(),
39+
'publication_date' => fake()->date(),
3940
];
4041
}
4142
}

src/Laravel/workbench/database/migrations/2023_07_15_231244_create_book_table.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public function up(): void
3131
$table->ulid('id')->primary();
3232
$table->string('name');
3333
$table->string('isbn');
34+
$table->date('publication_date');
3435
$table->integer('author_id')->unsigned();
3536
$table->foreign('author_id')->references('id')->on('authors');
3637
$table->timestamps();

src/Metadata/Util/CamelCaseToSnakeCaseNameConverter.php

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,14 @@
3131
*/
3232
class CamelCaseToSnakeCaseNameConverter
3333
{
34-
private $attributes;
35-
private $lowerCamelCase;
36-
3734
/**
3835
* @param array|null $attributes The list of attributes to rename or null for all attributes
3936
* @param bool $lowerCamelCase Use lowerCamelCase style
4037
*/
41-
public function __construct(?array $attributes = null, bool $lowerCamelCase = true)
42-
{
43-
$this->attributes = $attributes;
44-
$this->lowerCamelCase = $lowerCamelCase;
38+
public function __construct(
39+
private readonly ?array $attributes = null,
40+
private readonly bool $lowerCamelCase = true,
41+
) {
4542
}
4643

4744
public function normalize(string $propertyName): string
@@ -55,9 +52,11 @@ public function normalize(string $propertyName): string
5552

5653
public function denormalize(string $propertyName): string
5754
{
58-
$camelCasedName = preg_replace_callback('/(^|_|\.)+(.)/', function ($match) {
59-
return ('.' === $match[1] ? '_' : '').strtoupper($match[2]);
60-
}, $propertyName);
55+
$camelCasedName = preg_replace_callback(
56+
'/(^|_|\.)+(.)/',
57+
fn ($match) => ('.' === $match[1] ? '_' : '').strtoupper($match[2]),
58+
$propertyName
59+
);
6160

6261
if ($this->lowerCamelCase) {
6362
$camelCasedName = lcfirst($camelCasedName);

0 commit comments

Comments
 (0)