Skip to content

Commit 2e8287d

Browse files
committed
fix(laravel): allow serializer attributes through ApiProperty
1 parent 4ad7a50 commit 2e8287d

File tree

19 files changed

+469
-26
lines changed

19 files changed

+469
-26
lines changed

src/Laravel/ApiPlatformProvider.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@
165165
use ApiPlatform\Serializer\ItemNormalizer;
166166
use ApiPlatform\Serializer\JsonEncoder;
167167
use ApiPlatform\Serializer\Mapping\Factory\ClassMetadataFactory as SerializerClassMetadataFactory;
168+
use ApiPlatform\Serializer\Mapping\Loader\PropertyMetadataLoader;
168169
use ApiPlatform\Serializer\Parameter\SerializerFilterParameterProvider;
169170
use ApiPlatform\Serializer\SerializerContextBuilder;
170171
use ApiPlatform\State\CallableProcessor;
@@ -206,6 +207,7 @@
206207
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
207208
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
208209
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
210+
use Symfony\Component\Serializer\Mapping\Loader\LoaderChain;
209211
use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface;
210212
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
211213
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
@@ -244,8 +246,15 @@ public function register(): void
244246

245247
$this->app->bind(LoaderInterface::class, AttributeLoader::class);
246248
$this->app->bind(ClassMetadataFactoryInterface::class, ClassMetadataFactory::class);
247-
$this->app->singleton(ClassMetadataFactory::class, function () {
248-
return new ClassMetadataFactory(new AttributeLoader());
249+
$this->app->singleton(ClassMetadataFactory::class, function (Application $app) {
250+
return new ClassMetadataFactory(
251+
new LoaderChain([
252+
new PropertyMetadataLoader(
253+
$app->make(PropertyNameCollectionFactoryInterface::class),
254+
),
255+
new AttributeLoader(),
256+
])
257+
);
249258
});
250259

251260
$this->app->singleton(SerializerClassMetadataFactory::class, function (Application $app) {

src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyNameCollectionMetadataFactory.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,12 @@ public function create(string $resourceClass, array $options = []): PropertyName
3939
return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection();
4040
}
4141

42-
$refl = new \ReflectionClass($resourceClass);
4342
try {
43+
$refl = new \ReflectionClass($resourceClass);
44+
if ($refl->isAbstract()) {
45+
return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection();
46+
}
47+
4448
$model = $refl->newInstanceWithoutConstructor();
4549
} catch (\ReflectionException) {
4650
return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection();

src/Laravel/Tests/JsonApiTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Workbench\App\Models\Book;
2424
use Workbench\Database\Factories\AuthorFactory;
2525
use Workbench\Database\Factories\BookFactory;
26+
use Workbench\Database\Factories\WithAccessorFactory;
2627

2728
class JsonApiTest extends TestCase
2829
{
@@ -197,4 +198,15 @@ public function testDeleteBook(): void
197198
$response->assertStatus(204);
198199
$this->assertNull(Book::find($book->id));
199200
}
201+
202+
public function testRelationWithGroups(): void
203+
{
204+
WithAccessorFactory::new()->create();
205+
$response = $this->get('/api/with_accessors/1', ['accept' => 'application/vnd.api+json']);
206+
$content = $response->json();
207+
$this->assertArrayHasKey('data', $content);
208+
$this->assertArrayHasKey('relationships', $content['data']);
209+
$this->assertArrayHasKey('relation', $content['data']['relationships']);
210+
$this->assertArrayHasKey('data', $content['data']['relationships']['relation']);
211+
}
200212
}

src/Laravel/Tests/JsonLdTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Workbench\Database\Factories\CommentFactory;
2727
use Workbench\Database\Factories\PostFactory;
2828
use Workbench\Database\Factories\SluggableFactory;
29+
use Workbench\Database\Factories\WithAccessorFactory;
2930

3031
class JsonLdTest extends TestCase
3132
{
@@ -327,4 +328,13 @@ public function testError(): void
327328
$content = $response->json();
328329
$this->assertArrayHasKey('trace', $content);
329330
}
331+
332+
public function testRelationWithGroups(): void
333+
{
334+
WithAccessorFactory::new()->create();
335+
$response = $this->get('/api/with_accessors/1', ['accept' => 'application/ld+json']);
336+
$content = $response->json();
337+
$this->assertArrayHasKey('relation', $content);
338+
$this->assertArrayHasKey('name', $content['relation']);
339+
}
330340
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,28 @@
1313

1414
namespace Workbench\App\Models;
1515

16+
use ApiPlatform\Metadata\ApiProperty;
1617
use ApiPlatform\Metadata\ApiResource;
1718
use Illuminate\Database\Eloquent\Casts\Attribute;
1819
use Illuminate\Database\Eloquent\Factories\HasFactory;
1920
use Illuminate\Database\Eloquent\Model;
21+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
22+
use Symfony\Component\Serializer\Attribute\Groups;
2023

21-
#[ApiResource]
24+
#[ApiResource(normalizationContext: ['groups' => ['read']])]
2225
class WithAccessor extends Model
2326
{
2427
use HasFactory;
2528

2629
protected $hidden = ['created_at', 'updated_at', 'id'];
2730

31+
#[ApiProperty(serialize: [new Groups(['read'])])]
32+
public function relation(): BelongsTo
33+
{
34+
return $this->belongsTo(WithAccessorRelation::class);
35+
}
36+
37+
#[ApiProperty(serialize: [new Groups(['read'])])]
2838
protected function name(): Attribute
2939
{
3040
return Attribute::make(
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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 Workbench\App\Models;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use Illuminate\Database\Eloquent\Factories\HasFactory;
18+
use Illuminate\Database\Eloquent\Model;
19+
use Symfony\Component\Serializer\Attribute\Groups;
20+
21+
#[Groups(['read'])]
22+
#[ApiResource(operations: [])]
23+
class WithAccessorRelation extends Model
24+
{
25+
use HasFactory;
26+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public function definition(): array
3939
{
4040
return [
4141
'name' => strtolower(fake()->name()),
42+
'relation_id' => WithAccessorRelationFactory::new(),
4243
];
4344
}
4445
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 Workbench\Database\Factories;
15+
16+
use Illuminate\Database\Eloquent\Factories\Factory;
17+
use Workbench\App\Models\WithAccessorRelation;
18+
19+
/**
20+
* @template TModel of \Workbench\App\Models\WithAccessorRelation
21+
*
22+
* @extends \Illuminate\Database\Eloquent\Factories\Factory<TModel>
23+
*/
24+
class WithAccessorRelationFactory extends Factory
25+
{
26+
/**
27+
* The name of the factory's corresponding model.
28+
*
29+
* @var class-string<TModel>
30+
*/
31+
protected $model = WithAccessorRelation::class;
32+
33+
/**
34+
* Define the model's default state.
35+
*
36+
* @return array<string, mixed>
37+
*/
38+
public function definition(): array
39+
{
40+
return [
41+
'name' => strtolower(fake()->name()),
42+
];
43+
}
44+
}

src/Laravel/workbench/database/migrations/2024_09_24_065934_create_with_accessors_table.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,17 @@
2121
*/
2222
public function up(): void
2323
{
24+
Schema::create('with_accessor_relations', function (Blueprint $table): void {
25+
$table->id();
26+
$table->string('name');
27+
$table->timestamps();
28+
});
29+
2430
Schema::create('with_accessors', function (Blueprint $table): void {
2531
$table->id();
2632
$table->string('name');
33+
$table->integer('relation_id')->unsigned();
34+
$table->foreign('relation_id')->references('id')->on('with_accessor_relations');
2735
$table->timestamps();
2836
});
2937
}
@@ -34,5 +42,6 @@ public function up(): void
3442
public function down(): void
3543
{
3644
Schema::dropIfExists('with_accessors');
45+
Schema::dropIfExists('with_accessors_relation');
3746
}
3847
};

src/Metadata/ApiProperty.php

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
namespace ApiPlatform\Metadata;
1515

1616
use Symfony\Component\PropertyInfo\Type;
17+
use Symfony\Component\Serializer\Attribute\Context;
18+
use Symfony\Component\Serializer\Attribute\Groups;
19+
use Symfony\Component\Serializer\Attribute\Ignore;
20+
use Symfony\Component\Serializer\Attribute\MaxDepth;
21+
use Symfony\Component\Serializer\Attribute\SerializedName;
22+
use Symfony\Component\Serializer\Attribute\SerializedPath;
1723

1824
/**
1925
* ApiProperty annotation.
@@ -23,24 +29,28 @@
2329
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER | \Attribute::TARGET_CLASS_CONSTANT | \Attribute::TARGET_CLASS)]
2430
final class ApiProperty
2531
{
32+
private ?array $types;
33+
private ?array $serialize;
34+
2635
/**
27-
* @param bool|null $readableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations
28-
* @param bool|null $writableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations
29-
* @param bool|null $required https://api-platform.com/docs/admin/validation/#client-side-validation
30-
* @param bool|null $identifier https://api-platform.com/docs/core/identifiers/
31-
* @param mixed $example https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts
32-
* @param string|null $deprecationReason https://api-platform.com/docs/core/deprecations/#deprecating-resource-classes-operations-and-properties
33-
* @param bool|null $fetchEager https://api-platform.com/docs/core/performance/#eager-loading
34-
* @param array|null $jsonldContext https://api-platform.com/docs/core/extending-jsonld-context/#extending-json-ld-and-hydra-contexts
35-
* @param array|null $openapiContext https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts
36-
* @param bool|null $push https://api-platform.com/docs/core/push-relations/
37-
* @param string|\Stringable|null $security https://api-platform.com/docs/core/security
38-
* @param string|\Stringable|null $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization
39-
* @param string[] $types the RDF types of this property
40-
* @param string[] $iris
41-
* @param Type[] $builtinTypes
42-
* @param string|null $uriTemplate (experimental) whether to return the subRessource collection IRI instead of an iterable of IRI
43-
* @param string|null $property The property name
36+
* @param bool|null $readableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations
37+
* @param bool|null $writableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations
38+
* @param bool|null $required https://api-platform.com/docs/admin/validation/#client-side-validation
39+
* @param bool|null $identifier https://api-platform.com/docs/core/identifiers/
40+
* @param mixed $example https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts
41+
* @param string|null $deprecationReason https://api-platform.com/docs/core/deprecations/#deprecating-resource-classes-operations-and-properties
42+
* @param bool|null $fetchEager https://api-platform.com/docs/core/performance/#eager-loading
43+
* @param array|null $jsonldContext https://api-platform.com/docs/core/extending-jsonld-context/#extending-json-ld-and-hydra-contexts
44+
* @param array|null $openapiContext https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts
45+
* @param bool|null $push https://api-platform.com/docs/core/push-relations/
46+
* @param string|\Stringable|null $security https://api-platform.com/docs/core/security
47+
* @param string|\Stringable|null $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization
48+
* @param string[] $types the RDF types of this property
49+
* @param string[] $iris
50+
* @param Type[] $builtinTypes
51+
* @param string|null $uriTemplate (experimental) whether to return the subRessource collection IRI instead of an iterable of IRI
52+
* @param string|null $property The property name
53+
* @param Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth|array<array-key, Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth> $serialize Serializer attributes
4454
*/
4555
public function __construct(
4656
private ?string $description = null,
@@ -193,7 +203,7 @@ public function __construct(
193203
* </div>
194204
*/
195205
private string|\Stringable|null $securityPostDenormalize = null,
196-
private array|string|null $types = null,
206+
array|string|null $types = null,
197207
/*
198208
* The related php types.
199209
*/
@@ -205,11 +215,11 @@ public function __construct(
205215
private ?string $uriTemplate = null,
206216
private ?string $property = null,
207217
private ?string $policy = null,
218+
array|Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth|null $serialize = null,
208219
private array $extraProperties = [],
209220
) {
210-
if (\is_string($types)) {
211-
$this->types = (array) $types;
212-
}
221+
$this->types = \is_string($types) ? (array) $types : $types;
222+
$this->serialize = \is_array($serialize) ? $serialize : (array) $serialize;
213223
}
214224

215225
public function getProperty(): ?string
@@ -600,4 +610,20 @@ public function withPolicy(?string $policy): static
600610

601611
return $self;
602612
}
613+
614+
public function getSerialize(): ?array
615+
{
616+
return $this->serialize;
617+
}
618+
619+
/**
620+
* @param Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth|array<array-key, Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth> $serialize
621+
*/
622+
public function withSerialize(array|Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth $serialize): static
623+
{
624+
$self = clone $this;
625+
$self->serialize = (array) $serialize;
626+
627+
return $self;
628+
}
603629
}

0 commit comments

Comments
 (0)