From 00787f32da54418de7d869cff218e22d8ae2ae1d Mon Sep 17 00:00:00 2001 From: valentindrdt Date: Wed, 18 Sep 2024 09:02:54 -0700 Subject: [PATCH 1/7] feat(laravel): automatically register policies (#6623) * feat: if a policy matches the name of a model we automatically register it, no need to do it manually anymore * fix: optimising the way we handle collection * fix: phpstan * fix: cs-fixer --- ...quentResourceCollectionMetadataFactory.php | 27 ++++ .../Security/ResourceAccessChecker.php | 8 +- src/Laravel/Tests/Policy/BookAllowPolicy.php | 60 +++++++ src/Laravel/Tests/Policy/BookDenyPolicy.php | 60 +++++++ src/Laravel/Tests/Policy/PolicyAllowTest.php | 149 ++++++++++++++++++ src/Laravel/Tests/Policy/PolicyDenyTest.php | 135 ++++++++++++++++ 6 files changed, 438 insertions(+), 1 deletion(-) create mode 100644 src/Laravel/Tests/Policy/BookAllowPolicy.php create mode 100644 src/Laravel/Tests/Policy/BookDenyPolicy.php create mode 100644 src/Laravel/Tests/Policy/PolicyAllowTest.php create mode 100644 src/Laravel/Tests/Policy/PolicyDenyTest.php diff --git a/src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php b/src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php index 5a7ddb822bb..c693ff55ff5 100644 --- a/src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php +++ b/src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php @@ -18,13 +18,29 @@ use ApiPlatform\Laravel\Eloquent\State\PersistProcessor; use ApiPlatform\Laravel\Eloquent\State\RemoveProcessor; use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\DeleteOperationInterface; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Gate; final class EloquentResourceCollectionMetadataFactory implements ResourceMetadataCollectionFactoryInterface { + private const POLICY_METHODS = [ + Put::class => 'update', + Post::class => 'create', + Get::class => 'view', + GetCollection::class => 'viewAny', + Delete::class => 'delete', + Patch::class => 'update', + ]; + public function __construct( private readonly ResourceMetadataCollectionFactoryInterface $decorated, ) { @@ -55,6 +71,17 @@ public function create(string $resourceClass): ResourceMetadataCollection $operation = $operation->withProvider($operation instanceof CollectionOperationInterface ? CollectionProvider::class : ItemProvider::class); } + if (!$operation->getPolicy() && ($policy = Gate::getPolicyFor($model))) { + $policyMethod = self::POLICY_METHODS[$operation::class] ?? null; + if ($operation instanceof Put && $operation->getAllowCreate()) { + $policyMethod = self::POLICY_METHODS[Post::class]; + } + + if ($policyMethod && method_exists($policy, $policyMethod)) { + $operation = $operation->withPolicy($policyMethod); + } + } + if (!$operation->getProcessor()) { $operation = $operation->withProcessor($operation instanceof DeleteOperationInterface ? RemoveProcessor::class : PersistProcessor::class); } diff --git a/src/Laravel/Security/ResourceAccessChecker.php b/src/Laravel/Security/ResourceAccessChecker.php index 1a7c4d53541..5633ea1c808 100644 --- a/src/Laravel/Security/ResourceAccessChecker.php +++ b/src/Laravel/Security/ResourceAccessChecker.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Laravel\Security; +use ApiPlatform\Laravel\Eloquent\Paginator; use ApiPlatform\Metadata\ResourceAccessCheckerInterface; use Illuminate\Support\Facades\Gate; @@ -20,6 +21,11 @@ class ResourceAccessChecker implements ResourceAccessCheckerInterface { public function isGranted(string $resourceClass, string $expression, array $extraVariables = []): bool { - return Gate::allows($expression, $extraVariables['object']); + return Gate::allows( + $expression, + $extraVariables['object'] instanceof Paginator ? + $resourceClass : + $extraVariables['object'] + ); } } diff --git a/src/Laravel/Tests/Policy/BookAllowPolicy.php b/src/Laravel/Tests/Policy/BookAllowPolicy.php new file mode 100644 index 00000000000..bea0109c494 --- /dev/null +++ b/src/Laravel/Tests/Policy/BookAllowPolicy.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Policy; + +use Illuminate\Foundation\Auth\User; +use Workbench\App\Models\Book; + +class BookAllowPolicy +{ + /** + * Determine whether the user can view any models. + */ + public function viewAny(?User $user): bool + { + return true; + } + + /** + * Determine whether the user can view the model. + */ + public function view(?User $user, Book $book): bool + { + return true; + } + + /** + * Determine whether the user can create models. + */ + public function create(?User $user): bool + { + return true; + } + + /** + * Determine whether the user can update the model. + */ + public function update(?User $user, Book $book): bool + { + return true; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(?User $user, Book $book): bool + { + return true; + } +} diff --git a/src/Laravel/Tests/Policy/BookDenyPolicy.php b/src/Laravel/Tests/Policy/BookDenyPolicy.php new file mode 100644 index 00000000000..d9f70e82930 --- /dev/null +++ b/src/Laravel/Tests/Policy/BookDenyPolicy.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Policy; + +use Illuminate\Foundation\Auth\User; +use Workbench\App\Models\Book; + +class BookDenyPolicy +{ + /** + * Determine whether the user can view any models. + */ + public function viewAny(?User $user): bool + { + return false; + } + + /** + * Determine whether the user can view the model. + */ + public function view(?User $user, Book $book): bool + { + return false; + } + + /** + * Determine whether the user can create models. + */ + public function create(?User $user): bool + { + return false; + } + + /** + * Determine whether the user can update the model. + */ + public function update(?User $user, Book $book): bool + { + return false; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(?User $user, Book $book): bool + { + return false; + } +} diff --git a/src/Laravel/Tests/Policy/PolicyAllowTest.php b/src/Laravel/Tests/Policy/PolicyAllowTest.php new file mode 100644 index 00000000000..c5124a6d5e1 --- /dev/null +++ b/src/Laravel/Tests/Policy/PolicyAllowTest.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Policy; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Gate; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\App\Models\Author; +use Workbench\App\Models\Book; + +class PolicyAllowTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + Gate::guessPolicyNamesUsing(function (string $modelClass) { + return Book::class === $modelClass ? + BookAllowPolicy::class : + null; + }); + + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.formats', ['jsonapi' => ['application/vnd.api+json']]); + $config->set('api-platform.docs_formats', ['jsonapi' => ['application/vnd.api+json']]); + }); + } + + public function testGetCollection(): void + { + $response = $this->get('/api/books', ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(200); + } + + public function testGetEmptyColelction(): void + { + $response = $this->get('/api/books?publicationDate[gt]=9999-12-31', ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(200); + $response->assertJsonFragment([ + 'meta' => [ + 'totalItems' => 0, + 'itemsPerPage' => 5, + 'currentPage' => 1, + ], + ]); + } + + public function testGetBook(): void + { + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->get($iri, ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(200); + } + + public function testCreateBook(): void + { + $author = Author::find(1); + $response = $this->postJson( + '/api/books', + [ + 'data' => [ + 'attributes' => [ + 'name' => 'Don Quichotte', + 'isbn' => fake()->isbn13(), + 'publicationDate' => fake()->optional()->date(), + ], + 'relationships' => [ + 'author' => [ + 'data' => [ + 'id' => $this->getIriFromResource($author), + 'type' => 'Author', + ], + ], + ], + ], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + + $response->assertStatus(201); + } + + public function testUpdateBook(): void + { + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->putJson( + $iri, + [ + 'data' => ['attributes' => ['name' => 'updated title']], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + $response->assertStatus(200); + } + + public function testPatchBook(): void + { + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->patchJson( + $iri, + [ + 'name' => 'updated title', + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/merge-patch+json', + ] + ); + $response->assertStatus(200); + } + + public function testDeleteBook(): void + { + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->delete($iri, headers: ['accept' => 'application/vnd.api+json']); + $response->assertStatus(204); + $this->assertNull(Book::find($book->id)); + } +} diff --git a/src/Laravel/Tests/Policy/PolicyDenyTest.php b/src/Laravel/Tests/Policy/PolicyDenyTest.php new file mode 100644 index 00000000000..60d6eaa9ed9 --- /dev/null +++ b/src/Laravel/Tests/Policy/PolicyDenyTest.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Policy; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Gate; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\App\Models\Author; +use Workbench\App\Models\Book; + +class PolicyDenyTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + Gate::guessPolicyNamesUsing(function (string $modelClass) { + return Book::class === $modelClass ? + BookDenyPolicy::class : + null; + }); + + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.formats', ['jsonapi' => ['application/vnd.api+json']]); + $config->set('api-platform.docs_formats', ['jsonapi' => ['application/vnd.api+json']]); + }); + } + + public function testGetCollection(): void + { + $response = $this->get('/api/books', ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(403); + } + + public function testGetBook(): void + { + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->get($iri, ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(403); + } + + public function testCreateBook(): void + { + $author = Author::find(1); + $response = $this->postJson( + '/api/books', + [ + 'data' => [ + 'attributes' => [ + 'name' => 'Don Quichotte', + 'isbn' => fake()->isbn13(), + 'publicationDate' => fake()->optional()->date(), + ], + 'relationships' => [ + 'author' => [ + 'data' => [ + 'id' => $this->getIriFromResource($author), + 'type' => 'Author', + ], + ], + ], + ], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + + $response->assertStatus(403); + } + + public function testUpdateBook(): void + { + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->putJson( + $iri, + [ + 'data' => ['attributes' => ['name' => 'updated title']], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + $response->assertStatus(403); + } + + public function testPatchBook(): void + { + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->patchJson( + $iri, + [ + 'name' => 'updated title', + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/merge-patch+json', + ] + ); + $response->assertStatus(403); + } + + public function testDeleteBook(): void + { + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->delete($iri, headers: ['accept' => 'application/vnd.api+json']); + $response->assertStatus(403); + } +} From e370d20e35c99fd72028fafa769308d7598854a1 Mon Sep 17 00:00:00 2001 From: valentindrdt Date: Thu, 19 Sep 2024 07:48:28 -0700 Subject: [PATCH 2/7] feat: api-platform/json-hal component (#6621) * feat: add hal support for laravel * feat: quick review * fix: typo & cs-fixer * fix: typo in composer.json * fix: cs-fixer & phpstan * fix: forgot about hal item normalizer, therefore there's no more createbook nor updatebook test as Hal is a readonly format --- src/Hal/Serializer/ObjectNormalizer.php | 1 - src/Hal/composer.json | 62 +++++++++++++ src/Laravel/ApiPlatformProvider.php | 50 ++++++++++- src/Laravel/Tests/HalTest.php | 112 ++++++++++++++++++++++++ src/Laravel/composer.json | 1 + 5 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 src/Hal/composer.json create mode 100644 src/Laravel/Tests/HalTest.php diff --git a/src/Hal/Serializer/ObjectNormalizer.php b/src/Hal/Serializer/ObjectNormalizer.php index ea3d7a895b8..f52903b877f 100644 --- a/src/Hal/Serializer/ObjectNormalizer.php +++ b/src/Hal/Serializer/ObjectNormalizer.php @@ -17,7 +17,6 @@ use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Decorates the output with JSON HAL metadata when appropriate, but otherwise diff --git a/src/Hal/composer.json b/src/Hal/composer.json new file mode 100644 index 00000000000..df27693cd26 --- /dev/null +++ b/src/Hal/composer.json @@ -0,0 +1,62 @@ +{ + "name": "api-platform/json-hal", + "description": "API Hal support", + "type": "library", + "keywords": [ + "REST", + "API", + "HAL" + ], + "homepage": "https://api-platform.com", + "license": "MIT", + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "require": { + "php": ">=8.1", + "api-platform/state": "^3.4 || ^4.0", + "api-platform/metadata": "^3.4 || ^4.0", + "api-platform/serializer": "^3.4 || ^4.0" + }, + "autoload": { + "psr-4": { + "ApiPlatform\\Hal\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "config": { + "preferred-install": { + "*": "dist" + }, + "sort-packages": true, + "allow-plugins": { + "composer/package-versions-deprecated": true, + "phpstan/extension-installer": true + } + }, + "extra": { + "branch-alias": { + "dev-main": "4.0.x-dev", + "dev-3.4": "3.4.x-dev" + }, + "symfony": { + "require": "^6.4 || ^7.1" + } + }, + "scripts": { + "test": "./vendor/bin/phpunit" + }, + "require-dev": { + "phpunit/phpunit": "^11.2" + } +} diff --git a/src/Laravel/ApiPlatformProvider.php b/src/Laravel/ApiPlatformProvider.php index 9a53504e324..d11885ab54f 100644 --- a/src/Laravel/ApiPlatformProvider.php +++ b/src/Laravel/ApiPlatformProvider.php @@ -43,6 +43,10 @@ use ApiPlatform\GraphQl\Type\TypesContainerInterface; use ApiPlatform\GraphQl\Type\TypesFactory; use ApiPlatform\GraphQl\Type\TypesFactoryInterface; +use ApiPlatform\Hal\Serializer\CollectionNormalizer as HalCollectionNormalizer; +use ApiPlatform\Hal\Serializer\EntrypointNormalizer as HalEntrypointNormalizer; +use ApiPlatform\Hal\Serializer\ItemNormalizer as HalItemNormalizer; +use ApiPlatform\Hal\Serializer\ObjectNormalizer as HalObjectNormalizer; use ApiPlatform\Hydra\JsonSchema\SchemaFactory as HydraSchemaFactory; use ApiPlatform\Hydra\Serializer\CollectionFiltersNormalizer as HydraCollectionFiltersNormalizer; use ApiPlatform\Hydra\Serializer\CollectionNormalizer as HydraCollectionNormalizer; @@ -660,6 +664,43 @@ public function register(): void ); }); + $this->app->singleton(HalCollectionNormalizer::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new HalCollectionNormalizer( + $app->make(ResourceClassResolverInterface::class), + $config->get('api-platform.pagination.page_parameter_name'), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + ); + }); + + $this->app->singleton(HalObjectNormalizer::class, function (Application $app) { + return new HalObjectNormalizer( + $app->make(ObjectNormalizer::class), + $app->make(IriConverterInterface::class) + ); + }); + + $this->app->singleton(HalItemNormalizer::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new HalItemNormalizer( + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(IriConverterInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(PropertyAccessorInterface::class), + $app->make(NameConverterInterface::class), + $app->make(ClassMetadataFactoryInterface::class), + $defaultContext, + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(ResourceAccessCheckerInterface::class), + ); + }); + $this->app->singleton(Options::class, function (Application $app) { /** @var ConfigRepository */ $config = $app['config']; @@ -922,6 +963,10 @@ public function register(): void $list = new \SplPriorityQueue(); $list->insert($app->make(HydraEntrypointNormalizer::class), -800); $list->insert($app->make(HydraPartialCollectionViewNormalizer::class), -800); + $list->insert($app->make(HalCollectionNormalizer::class), -800); + $list->insert($app->make(HalEntrypointNormalizer::class), -985); + $list->insert($app->make(HalObjectNormalizer::class), -995); + $list->insert($app->make(HalItemNormalizer::class), -890); $list->insert($app->make(JsonLdItemNormalizer::class), -890); $list->insert($app->make(JsonLdObjectNormalizer::class), -995); $list->insert($app->make(ArrayDenormalizer::class), -990); @@ -950,10 +995,6 @@ public function register(): void // TODO: unused + implement hal/jsonapi ? // $list->insert($dataUriNormalizer, -920); // $list->insert($unwrappingDenormalizer, 1000); - // $list->insert($halItemNormalizer, -890); - // $list->insert($halEntrypointNormalizer, -800); - // $list->insert($halCollectionNormalizer, -985); - // $list->insert($halObjectNormalizer, -995); // $list->insert($jsonserializableNormalizer, -900); // $list->insert($uuidDenormalizer, -895); //Todo ramsey uuid support ? @@ -964,6 +1005,7 @@ public function register(): void $app->make(JsonEncoder::class), new JsonEncoder('jsonopenapi'), new JsonEncoder('jsonapi'), + new JsonEncoder('jsonhal'), new CsvEncoder(), ]); }); diff --git a/src/Laravel/Tests/HalTest.php b/src/Laravel/Tests/HalTest.php new file mode 100644 index 00000000000..4fc11f40461 --- /dev/null +++ b/src/Laravel/Tests/HalTest.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\App\Models\Book; + +class HalTest 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.formats', ['jsonhal' => ['application/hal+json']]); + $config->set('api-platform.docs_formats', ['jsonhal' => ['application/hal+json']]); + }); + } + + public function testGetEntrypoint(): void + { + $response = $this->get('/api/', ['accept' => ['application/hal+json']]); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/hal+json; charset=utf-8'); + + $this->assertJsonContains( + [ + '_links' => [ + 'self' => ['href' => '/api'], + 'book' => ['href' => '/api/books'], + 'post' => ['href' => '/api/posts'], + 'sluggable' => ['href' => '/api/sluggables'], + 'vault' => ['href' => '/api/vaults'], + 'author' => ['href' => '/api/authors'], + ], + ], + $response->json() + ); + } + + public function testGetCollection(): void + { + $response = $this->get('/api/books', ['accept' => 'application/hal+json']); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/hal+json; charset=utf-8'); + $this->assertJsonContains( + [ + '_links' => [ + 'first' => ['href' => '/api/books?page=1'], + 'self' => ['href' => '/api/books?page=1'], + 'last' => ['href' => '/api/books?page=2'], + ], + 'totalItems' => 10, + ], + $response->json() + ); + } + + public function testGetBook(): void + { + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->get($iri, ['accept' => ['application/hal+json']]); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/hal+json; charset=utf-8'); + $this->assertJsonContains( + [ + 'name' => $book->name, // @phpstan-ignore-line + 'isbn' => $book->isbn, // @phpstan-ignore-line + '_links' => [ + 'self' => [ + 'href' => $iri, + ], + 'author' => [ + 'href' => '/api/authors/1', + ], + ], + ], + $response->json() + ); + } + + public function testDeleteBook(): void + { + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->delete($iri, headers: ['accept' => 'application/hal+json']); + $response->assertStatus(204); + $this->assertNull(Book::find($book->id)); + } +} diff --git a/src/Laravel/composer.json b/src/Laravel/composer.json index 99f9cd65816..ce2378f9c0a 100644 --- a/src/Laravel/composer.json +++ b/src/Laravel/composer.json @@ -30,6 +30,7 @@ "php": ">=8.1", "api-platform/documentation": "^4.0", "api-platform/hydra": "^4.0", + "api-platform/json-hal": "^4.0", "api-platform/json-schema": "^4.0", "api-platform/jsonld": "^4.0", "api-platform/json-api": "^4.0", From 6f334fef225d7ca86ea5e8e1db4fc3e026355e68 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Wed, 2 Oct 2024 10:09:56 +0200 Subject: [PATCH 3/7] doc: mention alan PR in ADR [ci skip] --- docs/adr/0005-refactor-state-management.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0005-refactor-state-management.md b/docs/adr/0005-refactor-state-management.md index a4196044dab..f5549e17d64 100644 --- a/docs/adr/0005-refactor-state-management.md +++ b/docs/adr/0005-refactor-state-management.md @@ -30,7 +30,7 @@ For API Platform 3, we refactored the whole metadata susbsytem to be more flexib This led to the refactoring of the two main interfaces allowing to plug a data source in API Platform: the state provider and the state processor interfaces. Leveraging these new interfaces, it should be possible to simplify the code base and to remove most code duplication by transforming most of the code currently -stored in the kernel event listeners and in the GraphQL resolvers in dedicated state processors and state providers. +stored in the kernel event listeners and in the GraphQL resolvers in dedicated state processors and state providers. This is quite close to what @alanpoulain proposed in 2019 at https://github.com/api-platform/core/pull/2978 although at that time we needed to refactor the subresource system before tackling this issue. ## Decision Outcome From a4b79d1bbb000a81729c1d6b498b7d6f7748ed33 Mon Sep 17 00:00:00 2001 From: Simon <1218015+simondaigre@users.noreply.github.com> Date: Sat, 26 Oct 2024 09:08:32 +0200 Subject: [PATCH 4/7] chore: missing .gitattributes (#6757) --- src/Hal/.gitattributes | 5 +++++ src/HttpCache/.gitattributes | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 src/Hal/.gitattributes create mode 100644 src/HttpCache/.gitattributes diff --git a/src/Hal/.gitattributes b/src/Hal/.gitattributes new file mode 100644 index 00000000000..801f2080d71 --- /dev/null +++ b/src/Hal/.gitattributes @@ -0,0 +1,5 @@ +/.github export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/Tests export-ignore +/phpunit.xml.dist export-ignore diff --git a/src/HttpCache/.gitattributes b/src/HttpCache/.gitattributes new file mode 100644 index 00000000000..801f2080d71 --- /dev/null +++ b/src/HttpCache/.gitattributes @@ -0,0 +1,5 @@ +/.github export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/Tests export-ignore +/phpunit.xml.dist export-ignore From 67fbe51c570abe1ece6651ae6a037662e9012881 Mon Sep 17 00:00:00 2001 From: Deuchnord Date: Mon, 28 Oct 2024 11:16:59 +0100 Subject: [PATCH 5/7] fix: reintroduce the `show_webby` parameter in Laravel config (#6741) --- src/Laravel/config/api-platform.php | 1 + src/Laravel/resources/views/swagger-ui.blade.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/Laravel/config/api-platform.php b/src/Laravel/config/api-platform.php index 2d4b68783aa..bd1db3114ad 100644 --- a/src/Laravel/config/api-platform.php +++ b/src/Laravel/config/api-platform.php @@ -8,6 +8,7 @@ 'title' => 'API Platform', 'description' => 'My awesome API', 'version' => '1.0.0', + 'show_webby' => true, 'routes' => [ // Global middleware applied to every API Platform routes diff --git a/src/Laravel/resources/views/swagger-ui.blade.php b/src/Laravel/resources/views/swagger-ui.blade.php index acbe282d933..4a9436c6e0c 100644 --- a/src/Laravel/resources/views/swagger-ui.blade.php +++ b/src/Laravel/resources/views/swagger-ui.blade.php @@ -14,6 +14,7 @@
+ @if (config('api-platform.show_webby', true))
@@ -209,6 +210,7 @@ + @endif
From 8a4218b73520d8e7439eda3633736ee6014652b4 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 8 Nov 2024 10:56:01 +0100 Subject: [PATCH 6/7] ci: rename distribution workflow --- .github/workflows/api_platform.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/api_platform.yml b/.github/workflows/api_platform.yml index ebf3e3c27dc..654fa98c005 100644 --- a/.github/workflows/api_platform.yml +++ b/.github/workflows/api_platform.yml @@ -1,10 +1,13 @@ -name: CI +name: Distribution update on: push: tags: - v* +env: + GH_TOKEN: ${{ github.token }} + concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true From 96e88432a5385718ac253bc574ac2e7440d3617e Mon Sep 17 00:00:00 2001 From: nolotz Date: Sat, 16 Nov 2024 23:56:15 +0100 Subject: [PATCH 7/7] fix(laravel): add contact & license options to fix swagger UI issues --- src/Laravel/ApiPlatformProvider.php | 5 +++++ src/Laravel/config/api-platform.php | 11 ++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Laravel/ApiPlatformProvider.php b/src/Laravel/ApiPlatformProvider.php index 04534fee036..1eda99340bf 100644 --- a/src/Laravel/ApiPlatformProvider.php +++ b/src/Laravel/ApiPlatformProvider.php @@ -744,6 +744,11 @@ public function register(): void oAuthRefreshUrl: $config->get('api-platform.swagger_ui.oauth.refreshUrl', null), oAuthScopes: $config->get('api-platform.swagger_ui.oauth.scopes', []), apiKeys: $config->get('api-platform.swagger_ui.apiKeys', []), + contactName: $config->get('api-platform.swagger_ui.contact.name', ''), + contactUrl: $config->get('api-platform.swagger_ui.contact.url', ''), + contactEmail: $config->get('api-platform.swagger_ui.contact.email', ''), + licenseName: $config->get('api-platform.swagger_ui.license.name', ''), + licenseUrl: $config->get('api-platform.swagger_ui.license.url', ''), ); }); diff --git a/src/Laravel/config/api-platform.php b/src/Laravel/config/api-platform.php index bd1db3114ad..1531d0ed6d4 100644 --- a/src/Laravel/config/api-platform.php +++ b/src/Laravel/config/api-platform.php @@ -88,7 +88,16 @@ // 'refreshUrl' => '', // 'scopes' => ['scope1' => 'Description scope 1'], // 'pkce' => true - //] + //], + //'license' => [ + // 'name' => 'Apache 2.0', + // 'url' => 'https://www.apache.org/licenses/LICENSE-2.0.html', + //], + //'contact' => [ + // 'name' => 'API Support', + // 'url' => 'https://www.example.com/support', + // 'email' => 'support@example.com', + //], ], 'url_generation_strategy' => UrlGeneratorInterface::ABS_PATH,