Skip to content

Commit

Permalink
fix(laravel): allow serializer attributes through ApiProperty
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Sep 27, 2024
1 parent 2b4937a commit 6d39500
Show file tree
Hide file tree
Showing 17 changed files with 451 additions and 21 deletions.
13 changes: 11 additions & 2 deletions src/Laravel/ApiPlatformProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@
use ApiPlatform\Serializer\ItemNormalizer;
use ApiPlatform\Serializer\JsonEncoder;
use ApiPlatform\Serializer\Mapping\Factory\ClassMetadataFactory as SerializerClassMetadataFactory;
use ApiPlatform\Serializer\Mapping\Loader\PropertyMetadataLoader;
use ApiPlatform\Serializer\Parameter\SerializerFilterParameterProvider;
use ApiPlatform\Serializer\SerializerContextBuilder;
use ApiPlatform\State\CallableProcessor;
Expand Down Expand Up @@ -206,6 +207,7 @@
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
use Symfony\Component\Serializer\Mapping\Loader\LoaderChain;
use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
Expand Down Expand Up @@ -244,8 +246,15 @@ public function register(): void

$this->app->bind(LoaderInterface::class, AttributeLoader::class);
$this->app->bind(ClassMetadataFactoryInterface::class, ClassMetadataFactory::class);
$this->app->singleton(ClassMetadataFactory::class, function () {
return new ClassMetadataFactory(new AttributeLoader());
$this->app->singleton(ClassMetadataFactory::class, function (Application $app) {
return new ClassMetadataFactory(
new LoaderChain([
new PropertyMetadataLoader(
$app->make(PropertyNameCollectionFactoryInterface::class),
),
new AttributeLoader(),
])
);

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

View check run for this annotation

Codecov / codecov/patch

src/Laravel/ApiPlatformProvider.php#L249-L257

Added lines #L249 - L257 were not covered by tests
});

$this->app->singleton(SerializerClassMetadataFactory::class, function (Application $app) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,12 @@ public function create(string $resourceClass, array $options = []): PropertyName
return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection();
}

$refl = new \ReflectionClass($resourceClass);
try {
$refl = new \ReflectionClass($resourceClass);
if ($refl->isAbstract()) {
return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection();

Check warning on line 45 in src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyNameCollectionMetadataFactory.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyNameCollectionMetadataFactory.php#L43-L45

Added lines #L43 - L45 were not covered by tests
}

$model = $refl->newInstanceWithoutConstructor();
} catch (\ReflectionException) {
return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection();
Expand Down
9 changes: 9 additions & 0 deletions src/Laravel/Tests/JsonApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,13 @@ public function testDeleteBook(): void
$response->assertStatus(204);
$this->assertNull(Book::find($book->id));
}

public function testRelationWithGroups(): void

Check warning on line 192 in src/Laravel/Tests/JsonApiTest.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/Tests/JsonApiTest.php#L192

Added line #L192 was not covered by tests
{
$response = $this->get('/api/with_accessors/1', ['accept' => 'application/vnd.api+json']);
$content = $response->json();
$this->assertArrayHasKey('relationships', $content);
$this->assertArrayHasKey('relation', $content['relationships']);
$this->assertArrayHasKey('data', $content['relationships']['relation']);

Check warning on line 198 in src/Laravel/Tests/JsonApiTest.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/Tests/JsonApiTest.php#L194-L198

Added lines #L194 - L198 were not covered by tests
}
}
8 changes: 8 additions & 0 deletions src/Laravel/Tests/JsonLdTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -298,4 +298,12 @@ public function testError(): void
$content = $response->json();
$this->assertArrayHasKey('trace', $content);
}

public function testRelationWithGroups(): void

Check warning on line 302 in src/Laravel/Tests/JsonLdTest.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/Tests/JsonLdTest.php#L302

Added line #L302 was not covered by tests
{
$response = $this->get('/api/with_accessors/1', ['accept' => 'application/ld+json']);
$content = $response->json();
$this->assertArrayHasKey('relation', $content);
$this->assertArrayHasKey('name', $content['relation']);

Check warning on line 307 in src/Laravel/Tests/JsonLdTest.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/Tests/JsonLdTest.php#L304-L307

Added lines #L304 - L307 were not covered by tests
}
}
11 changes: 10 additions & 1 deletion src/Laravel/workbench/app/Models/WithAccessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,27 @@

namespace Workbench\App\Models;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Symfony\Component\Serializer\Attribute\Groups;

#[ApiResource]
#[ApiResource(normalizationContext: ['groups' => ['read']])]
class WithAccessor extends Model
{
use HasFactory;

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

#[ApiProperty(serialize: [new Groups(['read'])])]

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L31 was not covered by tests
public function relation(): BelongsTo
{
return $this->belongsTo(WithAccessorRelation::class);

Check warning on line 34 in src/Laravel/workbench/app/Models/WithAccessor.php

View check run for this annotation

Codecov / codecov/patch

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

Added line #L34 was not covered by tests
}

protected function name(): Attribute
{
return Attribute::make(
Expand Down
26 changes: 26 additions & 0 deletions src/Laravel/workbench/app/Models/WithAccessorRelation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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 Workbench\App\Models;

use ApiPlatform\Metadata\ApiResource;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Symfony\Component\Serializer\Attribute\Groups;

#[Groups(['read'])]
#[ApiResource(operations: [])]
class WithAccessorRelation extends Model
{
use HasFactory;
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public function definition(): array
{
return [
'name' => strtolower(fake()->name()),
'relation_id' => WithAccessorRelationFactory::new(),

Check warning on line 42 in src/Laravel/workbench/database/factories/WithAccessorFactory.php

View check run for this annotation

Codecov / codecov/patch

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

Added line #L42 was not covered by tests
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?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 Workbench\Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Workbench\App\Models\WithAccessorRelation;

/**
* @template TModel of \Workbench\App\Models\WithAccessorRelation
*
* @extends \Illuminate\Database\Eloquent\Factories\Factory<TModel>
*/
class WithAccessorRelationFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var class-string<TModel>
*/
protected $model = WithAccessorRelation::class;

/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array

Check warning on line 38 in src/Laravel/workbench/database/factories/WithAccessorRelationFactory.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/workbench/database/factories/WithAccessorRelationFactory.php#L38

Added line #L38 was not covered by tests
{
return [
'name' => strtolower(fake()->name()),
];

Check warning on line 42 in src/Laravel/workbench/database/factories/WithAccessorRelationFactory.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/workbench/database/factories/WithAccessorRelationFactory.php#L40-L42

Added lines #L40 - L42 were not covered by tests
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,17 @@
*/
public function up(): void
{
Schema::create('with_accessor_relations', function (Blueprint $table): void {
$table->id();
$table->string('name');
$table->timestamps();
});

Check warning on line 28 in src/Laravel/workbench/database/migrations/2024_09_24_065934_create_with_accessors_table.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/workbench/database/migrations/2024_09_24_065934_create_with_accessors_table.php#L24-L28

Added lines #L24 - L28 were not covered by tests

Schema::create('with_accessors', function (Blueprint $table): void {
$table->id();
$table->string('name');
$table->integer('relation_id')->unsigned();
$table->foreign('relation_id')->references('id')->on('with_accessor_relations');

Check warning on line 34 in src/Laravel/workbench/database/migrations/2024_09_24_065934_create_with_accessors_table.php

View check run for this annotation

Codecov / codecov/patch

src/Laravel/workbench/database/migrations/2024_09_24_065934_create_with_accessors_table.php#L33-L34

Added lines #L33 - L34 were not covered by tests
$table->timestamps();
});
}
Expand All @@ -34,5 +42,6 @@ public function up(): void
public function down(): void
{
Schema::dropIfExists('with_accessors');
Schema::dropIfExists('with_accessors_relation');

Check warning on line 45 in src/Laravel/workbench/database/migrations/2024_09_24_065934_create_with_accessors_table.php

View check run for this annotation

Codecov / codecov/patch

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

Added line #L45 was not covered by tests
}
};
58 changes: 41 additions & 17 deletions src/Metadata/ApiProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
namespace ApiPlatform\Metadata;

use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Attribute\Ignore;
use Symfony\Component\Serializer\Attribute\MaxDepth;
use Symfony\Component\Serializer\Attribute\SerializedName;
use Symfony\Component\Serializer\Attribute\SerializedPath;

/**
* ApiProperty annotation.
Expand All @@ -24,23 +30,24 @@
final class ApiProperty
{
/**
* @param bool|null $readableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations
* @param bool|null $writableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations
* @param bool|null $required https://api-platform.com/docs/admin/validation/#client-side-validation
* @param bool|null $identifier https://api-platform.com/docs/core/identifiers/
* @param mixed $example https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts
* @param string|null $deprecationReason https://api-platform.com/docs/core/deprecations/#deprecating-resource-classes-operations-and-properties
* @param bool|null $fetchEager https://api-platform.com/docs/core/performance/#eager-loading
* @param array|null $jsonldContext https://api-platform.com/docs/core/extending-jsonld-context/#extending-json-ld-and-hydra-contexts
* @param array|null $openapiContext https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts
* @param bool|null $push https://api-platform.com/docs/core/push-relations/
* @param string|\Stringable|null $security https://api-platform.com/docs/core/security
* @param string|\Stringable|null $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization
* @param string[] $types the RDF types of this property
* @param string[] $iris
* @param Type[] $builtinTypes
* @param string|null $uriTemplate (experimental) whether to return the subRessource collection IRI instead of an iterable of IRI
* @param string|null $property The property name
* @param bool|null $readableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations
* @param bool|null $writableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations
* @param bool|null $required https://api-platform.com/docs/admin/validation/#client-side-validation
* @param bool|null $identifier https://api-platform.com/docs/core/identifiers/
* @param mixed $example https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts
* @param string|null $deprecationReason https://api-platform.com/docs/core/deprecations/#deprecating-resource-classes-operations-and-properties
* @param bool|null $fetchEager https://api-platform.com/docs/core/performance/#eager-loading
* @param array|null $jsonldContext https://api-platform.com/docs/core/extending-jsonld-context/#extending-json-ld-and-hydra-contexts
* @param array|null $openapiContext https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts
* @param bool|null $push https://api-platform.com/docs/core/push-relations/
* @param string|\Stringable|null $security https://api-platform.com/docs/core/security
* @param string|\Stringable|null $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization
* @param string[] $types the RDF types of this property
* @param string[] $iris
* @param Type[] $builtinTypes
* @param string|null $uriTemplate (experimental) whether to return the subRessource collection IRI instead of an iterable of IRI
* @param string|null $property The property name
* @param array<int, Groups|SerializedName|SerializedPath|MaxDepth|Ignore|Context> $serialize
*/
public function __construct(
private ?string $description = null,
Expand Down Expand Up @@ -205,6 +212,7 @@ public function __construct(
private ?string $uriTemplate = null,
private ?string $property = null,
private ?string $policy = null,
private ?array $serialize = null,
private array $extraProperties = [],
) {
if (\is_string($types)) {
Expand Down Expand Up @@ -600,4 +608,20 @@ public function withPolicy(?string $policy): static

return $self;
}

public function getSerialize(): ?array

Check warning on line 612 in src/Metadata/ApiProperty.php

View check run for this annotation

Codecov / codecov/patch

src/Metadata/ApiProperty.php#L612

Added line #L612 was not covered by tests
{
return $this->serialize;

Check warning on line 614 in src/Metadata/ApiProperty.php

View check run for this annotation

Codecov / codecov/patch

src/Metadata/ApiProperty.php#L614

Added line #L614 was not covered by tests
}

/**
* @param array<int, Groups|SerializedName|SerializedPath|MaxDepth|Ignore|Context> $serialize
*/
public function withSerialize(array $serialize): static

Check warning on line 620 in src/Metadata/ApiProperty.php

View check run for this annotation

Codecov / codecov/patch

src/Metadata/ApiProperty.php#L620

Added line #L620 was not covered by tests
{
$self = clone $this;
$self->serialize = $serialize;

Check warning on line 623 in src/Metadata/ApiProperty.php

View check run for this annotation

Codecov / codecov/patch

src/Metadata/ApiProperty.php#L622-L623

Added lines #L622 - L623 were not covered by tests

return $self;

Check warning on line 625 in src/Metadata/ApiProperty.php

View check run for this annotation

Codecov / codecov/patch

src/Metadata/ApiProperty.php#L625

Added line #L625 was not covered by tests
}
}
Loading

0 comments on commit 6d39500

Please sign in to comment.