Skip to content

Commit

Permalink
feat: api-platform/json-hal component (#6621)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
valentindrdt authored Sep 19, 2024
1 parent 00787f3 commit e370d20
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 5 deletions.
1 change: 0 additions & 1 deletion src/Hal/Serializer/ObjectNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 62 additions & 0 deletions src/Hal/composer.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
50 changes: 46 additions & 4 deletions src/Laravel/ApiPlatformProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'];
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 ?
Expand All @@ -964,6 +1005,7 @@ public function register(): void
$app->make(JsonEncoder::class),
new JsonEncoder('jsonopenapi'),
new JsonEncoder('jsonapi'),
new JsonEncoder('jsonhal'),
new CsvEncoder(),
]);
});
Expand Down
112 changes: 112 additions & 0 deletions src/Laravel/Tests/HalTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?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 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));
}
}
1 change: 1 addition & 0 deletions src/Laravel/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit e370d20

Please sign in to comment.