Skip to content

Commit 2c3e9bb

Browse files
committed
fix(laravel): serialization issue with camelCase relation
fixes #7344
1 parent 126a126 commit 2c3e9bb

File tree

13 files changed

+281
-7
lines changed

13 files changed

+281
-7
lines changed

src/Laravel/ApiPlatformProvider.php

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
use ApiPlatform\Laravel\Eloquent\PropertyAccess\PropertyAccessor as EloquentPropertyAccessor;
8989
use ApiPlatform\Laravel\Eloquent\PropertyInfo\EloquentExtractor;
9090
use ApiPlatform\Laravel\Eloquent\Serializer\EloquentNameConverter;
91+
use ApiPlatform\Laravel\Eloquent\Serializer\Mapping\Loader\RelationMetadataLoader;
9192
use ApiPlatform\Laravel\Eloquent\Serializer\SerializerContextBuilder as EloquentSerializerContextBuilder;
9293
use ApiPlatform\Laravel\GraphQl\Controller\EntrypointController as GraphQlEntrypointController;
9394
use ApiPlatform\Laravel\GraphQl\Controller\GraphiQlController;
@@ -218,12 +219,21 @@ public function register(): void
218219
$this->app->bind(LoaderInterface::class, AttributeLoader::class);
219220
$this->app->bind(ClassMetadataFactoryInterface::class, ClassMetadataFactory::class);
220221
$this->app->singleton(ClassMetadataFactory::class, function (Application $app) {
222+
/** @var ConfigRepository */
223+
$config = $app['config'];
224+
$nameConverter = $config->get('api-platform.name_converter', SnakeCaseToCamelCaseNameConverter::class);
225+
if ($nameConverter && class_exists($nameConverter)) {
226+
$nameConverter = new EloquentNameConverter($app->make($nameConverter));
227+
}
228+
221229
return new ClassMetadataFactory(
222230
new LoaderChain([
223231
new PropertyMetadataLoader(
224232
$app->make(PropertyNameCollectionFactoryInterface::class),
233+
$nameConverter
225234
),
226235
new AttributeLoader(),
236+
// new RelationMetadataLoader($app->make(ModelMetadata::class)),
227237
])
228238
);
229239
});
@@ -261,6 +271,10 @@ public function register(): void
261271
$this->app->singleton(PropertyMetadataFactoryInterface::class, function (Application $app) {
262272
/** @var ConfigRepository $config */
263273
$config = $app['config'];
274+
$nameConverter = $config->get('api-platform.name_converter', SnakeCaseToCamelCaseNameConverter::class);
275+
if ($nameConverter && class_exists($nameConverter)) {
276+
$nameConverter = new EloquentNameConverter($app->make($nameConverter));
277+
}
264278

265279
return new CachePropertyMetadataFactory(
266280
new SchemaPropertyMetadataFactory(
@@ -274,7 +288,8 @@ public function register(): void
274288
new EloquentPropertyMetadataFactory(
275289
$app->make(ModelMetadata::class),
276290
),
277-
)
291+
),
292+
$nameConverter
278293
),
279294
$app->make(ResourceClassResolverInterface::class)
280295
),
@@ -287,6 +302,10 @@ public function register(): void
287302
$this->app->singleton(PropertyNameCollectionFactoryInterface::class, function (Application $app) {
288303
/** @var ConfigRepository $config */
289304
$config = $app['config'];
305+
$nameConverter = $config->get('api-platform.name_converter', SnakeCaseToCamelCaseNameConverter::class);
306+
if ($nameConverter && class_exists($nameConverter)) {
307+
$nameConverter = new EloquentNameConverter($app->make($nameConverter));
308+
}
290309

291310
return new CachePropertyNameCollectionMetadataFactory(
292311
new ClassLevelAttributePropertyNameCollectionFactory(
@@ -296,7 +315,8 @@ public function register(): void
296315
new PropertyInfoPropertyNameCollectionFactory($app->make(PropertyInfoExtractorInterface::class)),
297316
$app->make(ResourceClassResolverInterface::class)
298317
)
299-
)
318+
),
319+
$nameConverter
300320
),
301321
true === $config->get('app.debug') ? 'array' : $config->get('api-platform.cache', 'file')
302322
);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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\Mapping\ClassMetadataInterface;
17+
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
18+
19+
class SerializerClassMetadataFactory implements ClassMetadataFactoryInterface
20+
{
21+
public function __construct(private readonly ClassMetadataFactoryInterface $decorated)
22+
{
23+
}
24+
25+
/**
26+
* {@inheritdoc}
27+
*/
28+
public function getMetadataFor($value): ClassMetadataInterface
29+
{
30+
return $this->decorated->getMetadataFor($value);
31+
}
32+
33+
/**
34+
* {@inheritdoc}
35+
*/
36+
public function hasMetadataFor(mixed $value): bool
37+
{
38+
return $this->decorated->hasMetadataFor($value);
39+
}
40+
}

src/Laravel/Tests/EloquentTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
use Workbench\Database\Factories\CommentMorphFactory;
2626
use Workbench\Database\Factories\GrandSonFactory;
2727
use Workbench\Database\Factories\PostWithMorphManyFactory;
28+
use Workbench\App\Models\DeliveryRequest;
29+
use Workbench\App\Models\TimeSlot;
30+
use Workbench\Database\Factories\DeliveryRequestFactory;
31+
use Workbench\Database\Factories\TimeSlotFactory;
2832
use Workbench\Database\Factories\WithAccessorFactory;
2933

3034
class EloquentTest extends TestCase
@@ -589,4 +593,28 @@ public function testPostCommentItemFromMorphMany(): void
589593
'id' => 1,
590594
]);
591595
}
596+
597+
public function testCreateDeliveryRequestWithPickupSlot(): void
598+
{
599+
$pickupTimeSlot = TimeSlotFactory::new()->create(['note' => 'Morning slot']);
600+
601+
$response = $this->postJson('/api/delivery_requests', [
602+
'pickupTimeSlot' => '/api/time_slots/'.$pickupTimeSlot->id,
603+
'note' => 'This is a test note.',
604+
], ['accept' => 'application/ld+json', 'content-type' => 'application/ld+json']);
605+
606+
$response->assertStatus(201);
607+
$response->assertJson([
608+
'@context' => '/api/contexts/DeliveryRequest',
609+
'@id' => '/api/delivery_requests/1',
610+
'@type' => 'DeliveryRequest',
611+
'pickupTimeSlot' => [
612+
'@id' => '/api/time_slots/'.$pickupTimeSlot->id,
613+
'@type' => 'TimeSlot',
614+
'name' => $pickupTimeSlot->name,
615+
'note' => $pickupTimeSlot->note,
616+
],
617+
'note' => 'This is a test note.',
618+
]);
619+
}
592620
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@
1616
use ApiPlatform\Laravel\Eloquent\Filter\DateFilter;
1717
use ApiPlatform\Laravel\Eloquent\Filter\OrderFilter;
1818
use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter;
19+
use ApiPlatform\Metadata\ApiProperty;
1920
use ApiPlatform\Metadata\IsApiResource;
2021
use ApiPlatform\Metadata\QueryParameter;
2122
use Illuminate\Database\Eloquent\Factories\HasFactory;
2223
use Illuminate\Database\Eloquent\Model;
24+
use Symfony\Component\Serializer\Attribute\Groups;
2325

2426
#[QueryParameter(key: ':property', filter: PartialSearchFilter::class)]
2527
#[QueryParameter(key: 'createdAt', filter: DateFilter::class)]
2628
#[QueryParameter(key: 'order[:property]', filter: OrderFilter::class)]
29+
#[ApiProperty(property: 'name', serialize: [new Groups('read')])]
2730
class Author extends Model
2831
{
2932
use HasFactory;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Workbench\App\Models;
6+
7+
use ApiPlatform\Metadata\ApiProperty;
8+
use ApiPlatform\Metadata\ApiResource;
9+
use ApiPlatform\Metadata\Post;
10+
use Illuminate\Database\Eloquent\Factories\HasFactory;
11+
use Illuminate\Database\Eloquent\Model;
12+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
13+
use Symfony\Component\Serializer\Attribute\Groups;
14+
15+
#[ApiResource(
16+
operations: [
17+
new Post(
18+
normalizationContext: [
19+
'groups' => [
20+
'delivery_request:read',
21+
]
22+
],
23+
denormalizationContext: [
24+
'groups' => [
25+
'delivery_request:write',
26+
]
27+
]
28+
),
29+
]
30+
)]
31+
#[ApiProperty(property: 'pickupTimeSlot', serialize: new Groups(['delivery_request:read', 'delivery_request:write']))]
32+
#[ApiProperty(property: 'note', serialize: new Groups(['delivery_request:read', 'delivery_request:write']))]
33+
class DeliveryRequest extends Model
34+
{
35+
use HasFactory;
36+
37+
public function pickupTimeSlot(): BelongsTo
38+
{
39+
return $this->belongsTo(TimeSlot::class, 'pickup_time_slot_id');
40+
}
41+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Workbench\App\Models;
6+
7+
use ApiPlatform\Metadata\ApiResource;
8+
use Illuminate\Database\Eloquent\Factories\HasFactory;
9+
use Illuminate\Database\Eloquent\Model;
10+
11+
#[ApiResource]
12+
class Slot extends Model
13+
{
14+
use HasFactory;
15+
16+
protected $fillable = [
17+
'name',
18+
'note',
19+
];
20+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Workbench\App\Models;
6+
7+
use ApiPlatform\Metadata\ApiProperty;
8+
use ApiPlatform\Metadata\ApiResource;
9+
use Illuminate\Database\Eloquent\Factories\HasFactory;
10+
use Illuminate\Database\Eloquent\Model;
11+
use Symfony\Component\Serializer\Attribute\Groups;
12+
13+
#[ApiResource]
14+
#[ApiProperty(property: 'name', serialize: new Groups(['delivery_request:read', 'delivery_request:write']))]
15+
#[ApiProperty(property: 'note', serialize: new Groups(['delivery_request:read', 'delivery_request:write']))]
16+
class TimeSlot extends Model
17+
{
18+
use HasFactory;
19+
20+
protected $fillable = [
21+
'name',
22+
'note',
23+
];
24+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Workbench\Database\Factories;
6+
7+
use Illuminate\Database\Eloquent\Factories\Factory;
8+
use Workbench\App\Models\DeliveryRequest;
9+
10+
class DeliveryRequestFactory extends Factory
11+
{
12+
protected $model = DeliveryRequest::class;
13+
14+
public function definition(): array
15+
{
16+
return [
17+
'note' => $this->faker->sentence(),
18+
];
19+
}
20+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Workbench\Database\Factories;
6+
7+
use Illuminate\Database\Eloquent\Factories\Factory;
8+
use Workbench\App\Models\TimeSlot;
9+
10+
class TimeSlotFactory extends Factory
11+
{
12+
protected $model = TimeSlot::class;
13+
14+
public function definition(): array
15+
{
16+
return [
17+
'name' => $this->faker->word(),
18+
'note' => $this->faker->sentence(),
19+
];
20+
}
21+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
use Illuminate\Database\Migrations\Migration;
15+
use Illuminate\Database\Schema\Blueprint;
16+
use Illuminate\Support\Facades\Schema;
17+
18+
return new class extends Migration {
19+
/**
20+
* Run the migrations.
21+
*/
22+
public function up(): void
23+
{
24+
Schema::create('time_slots', function (Blueprint $table): void {
25+
$table->id();
26+
$table->string('name');
27+
$table->string('note')->nullable();
28+
$table->timestamps();
29+
});
30+
31+
Schema::create('delivery_requests', function (Blueprint $table): void {
32+
$table->id();
33+
$table->foreignId('pickup_time_slot_id')->nullable()->constrained('time_slots');
34+
$table->string('note')->nullable();
35+
$table->timestamps();
36+
});
37+
}
38+
39+
/**
40+
* Reverse the migrations.
41+
*/
42+
public function down(): void
43+
{
44+
Schema::dropIfExists('delivery_requests');
45+
Schema::dropIfExists('time_slots');
46+
}
47+
};

0 commit comments

Comments
 (0)