Skip to content

Commit e370d20

Browse files
authored
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
1 parent 00787f3 commit e370d20

File tree

5 files changed

+221
-5
lines changed

5 files changed

+221
-5
lines changed

src/Hal/Serializer/ObjectNormalizer.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
use Symfony\Component\Serializer\Exception\LogicException;
1818
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
1919
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
20-
use Symfony\Component\Serializer\Serializer;
2120

2221
/**
2322
* Decorates the output with JSON HAL metadata when appropriate, but otherwise

src/Hal/composer.json

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"name": "api-platform/json-hal",
3+
"description": "API Hal support",
4+
"type": "library",
5+
"keywords": [
6+
"REST",
7+
"API",
8+
"HAL"
9+
],
10+
"homepage": "https://api-platform.com",
11+
"license": "MIT",
12+
"authors": [
13+
{
14+
"name": "Kévin Dunglas",
15+
"email": "[email protected]",
16+
"homepage": "https://dunglas.fr"
17+
},
18+
{
19+
"name": "API Platform Community",
20+
"homepage": "https://api-platform.com/community/contributors"
21+
}
22+
],
23+
"require": {
24+
"php": ">=8.1",
25+
"api-platform/state": "^3.4 || ^4.0",
26+
"api-platform/metadata": "^3.4 || ^4.0",
27+
"api-platform/serializer": "^3.4 || ^4.0"
28+
},
29+
"autoload": {
30+
"psr-4": {
31+
"ApiPlatform\\Hal\\": ""
32+
},
33+
"exclude-from-classmap": [
34+
"/Tests/"
35+
]
36+
},
37+
"config": {
38+
"preferred-install": {
39+
"*": "dist"
40+
},
41+
"sort-packages": true,
42+
"allow-plugins": {
43+
"composer/package-versions-deprecated": true,
44+
"phpstan/extension-installer": true
45+
}
46+
},
47+
"extra": {
48+
"branch-alias": {
49+
"dev-main": "4.0.x-dev",
50+
"dev-3.4": "3.4.x-dev"
51+
},
52+
"symfony": {
53+
"require": "^6.4 || ^7.1"
54+
}
55+
},
56+
"scripts": {
57+
"test": "./vendor/bin/phpunit"
58+
},
59+
"require-dev": {
60+
"phpunit/phpunit": "^11.2"
61+
}
62+
}

src/Laravel/ApiPlatformProvider.php

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
4343
use ApiPlatform\GraphQl\Type\TypesContainerInterface;
4444
use ApiPlatform\GraphQl\Type\TypesFactory;
4545
use ApiPlatform\GraphQl\Type\TypesFactoryInterface;
46+
use ApiPlatform\Hal\Serializer\CollectionNormalizer as HalCollectionNormalizer;
47+
use ApiPlatform\Hal\Serializer\EntrypointNormalizer as HalEntrypointNormalizer;
48+
use ApiPlatform\Hal\Serializer\ItemNormalizer as HalItemNormalizer;
49+
use ApiPlatform\Hal\Serializer\ObjectNormalizer as HalObjectNormalizer;
4650
use ApiPlatform\Hydra\JsonSchema\SchemaFactory as HydraSchemaFactory;
4751
use ApiPlatform\Hydra\Serializer\CollectionFiltersNormalizer as HydraCollectionFiltersNormalizer;
4852
use ApiPlatform\Hydra\Serializer\CollectionNormalizer as HydraCollectionNormalizer;
@@ -660,6 +664,43 @@ public function register(): void
660664
);
661665
});
662666

667+
$this->app->singleton(HalCollectionNormalizer::class, function (Application $app) {
668+
/** @var ConfigRepository */
669+
$config = $app['config'];
670+
671+
return new HalCollectionNormalizer(
672+
$app->make(ResourceClassResolverInterface::class),
673+
$config->get('api-platform.pagination.page_parameter_name'),
674+
$app->make(ResourceMetadataCollectionFactoryInterface::class),
675+
);
676+
});
677+
678+
$this->app->singleton(HalObjectNormalizer::class, function (Application $app) {
679+
return new HalObjectNormalizer(
680+
$app->make(ObjectNormalizer::class),
681+
$app->make(IriConverterInterface::class)
682+
);
683+
});
684+
685+
$this->app->singleton(HalItemNormalizer::class, function (Application $app) {
686+
/** @var ConfigRepository */
687+
$config = $app['config'];
688+
$defaultContext = $config->get('api-platform.serializer', []);
689+
690+
return new HalItemNormalizer(
691+
$app->make(PropertyNameCollectionFactoryInterface::class),
692+
$app->make(PropertyMetadataFactoryInterface::class),
693+
$app->make(IriConverterInterface::class),
694+
$app->make(ResourceClassResolverInterface::class),
695+
$app->make(PropertyAccessorInterface::class),
696+
$app->make(NameConverterInterface::class),
697+
$app->make(ClassMetadataFactoryInterface::class),
698+
$defaultContext,
699+
$app->make(ResourceMetadataCollectionFactoryInterface::class),
700+
$app->make(ResourceAccessCheckerInterface::class),
701+
);
702+
});
703+
663704
$this->app->singleton(Options::class, function (Application $app) {
664705
/** @var ConfigRepository */
665706
$config = $app['config'];
@@ -922,6 +963,10 @@ public function register(): void
922963
$list = new \SplPriorityQueue();
923964
$list->insert($app->make(HydraEntrypointNormalizer::class), -800);
924965
$list->insert($app->make(HydraPartialCollectionViewNormalizer::class), -800);
966+
$list->insert($app->make(HalCollectionNormalizer::class), -800);
967+
$list->insert($app->make(HalEntrypointNormalizer::class), -985);
968+
$list->insert($app->make(HalObjectNormalizer::class), -995);
969+
$list->insert($app->make(HalItemNormalizer::class), -890);
925970
$list->insert($app->make(JsonLdItemNormalizer::class), -890);
926971
$list->insert($app->make(JsonLdObjectNormalizer::class), -995);
927972
$list->insert($app->make(ArrayDenormalizer::class), -990);
@@ -950,10 +995,6 @@ public function register(): void
950995
// TODO: unused + implement hal/jsonapi ?
951996
// $list->insert($dataUriNormalizer, -920);
952997
// $list->insert($unwrappingDenormalizer, 1000);
953-
// $list->insert($halItemNormalizer, -890);
954-
// $list->insert($halEntrypointNormalizer, -800);
955-
// $list->insert($halCollectionNormalizer, -985);
956-
// $list->insert($halObjectNormalizer, -995);
957998
// $list->insert($jsonserializableNormalizer, -900);
958999
// $list->insert($uuidDenormalizer, -895); //Todo ramsey uuid support ?
9591000
@@ -964,6 +1005,7 @@ public function register(): void
9641005
$app->make(JsonEncoder::class),
9651006
new JsonEncoder('jsonopenapi'),
9661007
new JsonEncoder('jsonapi'),
1008+
new JsonEncoder('jsonhal'),
9671009
new CsvEncoder(),
9681010
]);
9691011
});

src/Laravel/Tests/HalTest.php

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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\Tests;
15+
16+
use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait;
17+
use Illuminate\Contracts\Config\Repository;
18+
use Illuminate\Foundation\Application;
19+
use Illuminate\Foundation\Testing\RefreshDatabase;
20+
use Orchestra\Testbench\Concerns\WithWorkbench;
21+
use Orchestra\Testbench\TestCase;
22+
use Workbench\App\Models\Book;
23+
24+
class HalTest extends TestCase
25+
{
26+
use ApiTestAssertionsTrait;
27+
use RefreshDatabase;
28+
use WithWorkbench;
29+
30+
/**
31+
* @param Application $app
32+
*/
33+
protected function defineEnvironment($app): void
34+
{
35+
tap($app['config'], function (Repository $config): void {
36+
$config->set('api-platform.formats', ['jsonhal' => ['application/hal+json']]);
37+
$config->set('api-platform.docs_formats', ['jsonhal' => ['application/hal+json']]);
38+
});
39+
}
40+
41+
public function testGetEntrypoint(): void
42+
{
43+
$response = $this->get('/api/', ['accept' => ['application/hal+json']]);
44+
$response->assertStatus(200);
45+
$response->assertHeader('content-type', 'application/hal+json; charset=utf-8');
46+
47+
$this->assertJsonContains(
48+
[
49+
'_links' => [
50+
'self' => ['href' => '/api'],
51+
'book' => ['href' => '/api/books'],
52+
'post' => ['href' => '/api/posts'],
53+
'sluggable' => ['href' => '/api/sluggables'],
54+
'vault' => ['href' => '/api/vaults'],
55+
'author' => ['href' => '/api/authors'],
56+
],
57+
],
58+
$response->json()
59+
);
60+
}
61+
62+
public function testGetCollection(): void
63+
{
64+
$response = $this->get('/api/books', ['accept' => 'application/hal+json']);
65+
$response->assertStatus(200);
66+
$response->assertHeader('content-type', 'application/hal+json; charset=utf-8');
67+
$this->assertJsonContains(
68+
[
69+
'_links' => [
70+
'first' => ['href' => '/api/books?page=1'],
71+
'self' => ['href' => '/api/books?page=1'],
72+
'last' => ['href' => '/api/books?page=2'],
73+
],
74+
'totalItems' => 10,
75+
],
76+
$response->json()
77+
);
78+
}
79+
80+
public function testGetBook(): void
81+
{
82+
$book = Book::first();
83+
$iri = $this->getIriFromResource($book);
84+
$response = $this->get($iri, ['accept' => ['application/hal+json']]);
85+
$response->assertStatus(200);
86+
$response->assertHeader('content-type', 'application/hal+json; charset=utf-8');
87+
$this->assertJsonContains(
88+
[
89+
'name' => $book->name, // @phpstan-ignore-line
90+
'isbn' => $book->isbn, // @phpstan-ignore-line
91+
'_links' => [
92+
'self' => [
93+
'href' => $iri,
94+
],
95+
'author' => [
96+
'href' => '/api/authors/1',
97+
],
98+
],
99+
],
100+
$response->json()
101+
);
102+
}
103+
104+
public function testDeleteBook(): void
105+
{
106+
$book = Book::first();
107+
$iri = $this->getIriFromResource($book);
108+
$response = $this->delete($iri, headers: ['accept' => 'application/hal+json']);
109+
$response->assertStatus(204);
110+
$this->assertNull(Book::find($book->id));
111+
}
112+
}

src/Laravel/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"php": ">=8.1",
3131
"api-platform/documentation": "^4.0",
3232
"api-platform/hydra": "^4.0",
33+
"api-platform/json-hal": "^4.0",
3334
"api-platform/json-schema": "^4.0",
3435
"api-platform/jsonld": "^4.0",
3536
"api-platform/json-api": "^4.0",

0 commit comments

Comments
 (0)