Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .commitlintrc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"graphql",
"hal",
"httpcache",
"httpcache",
"hydra",
"jsonapi",
"jsonld",
Expand All @@ -26,6 +25,7 @@
"state",
"symfony",
"test",
"toon",
"validator",
]
],
Expand Down
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"api-platform/serializer": "self.version",
"api-platform/state": "self.version",
"api-platform/symfony": "self.version",
"api-platform/toon": "self.version",
"api-platform/validator": "self.version"
},
"require": {
Expand Down Expand Up @@ -137,6 +138,7 @@
"friends-of-behat/symfony-extension": "^2.1",
"friendsofphp/php-cs-fixer": "^3.93",
"guzzlehttp/guzzle": "^6.0 || ^7.0",
"helgesverre/toon": "^3.1",
"illuminate/config": "^11.0 || ^12.0",
"illuminate/contracts": "^11.0 || ^12.0",
"illuminate/database": "^11.0 || ^12.0",
Expand Down Expand Up @@ -201,6 +203,7 @@
"suggest": {
"doctrine/mongodb-odm-bundle": "To support MongoDB. Only versions 4.0 and later are supported.",
"elasticsearch/elasticsearch": "To support Elasticsearch.",
"helgesverre/toon": "To support Toon format serialization.",
"phpstan/phpdoc-parser": "To support extracting metadata from PHPDoc.",
"psr/cache-implementation": "To use metadata caching.",
"ramsey/uuid": "To support Ramsey's UUID identifiers.",
Expand Down
19 changes: 19 additions & 0 deletions src/Laravel/ApiPlatformProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@
use ApiPlatform\State\Provider\ReadProvider;
use ApiPlatform\State\ProviderInterface;
use ApiPlatform\State\SerializerContextBuilderInterface;
use ApiPlatform\Toon\Serializer\ToonEncoder;
use ApiPlatform\Toon\Serializer\ToonHydraCollectionNormalizer;
use ApiPlatform\Toon\Serializer\ToonHydraEntrypointNormalizer;
use ApiPlatform\Toon\Serializer\ToonJsonApiCollectionNormalizer;
use ApiPlatform\Toon\Serializer\ToonJsonApiEntrypointNormalizer;
use ApiPlatform\Toon\Serializer\ToonJsonApiItemNormalizer;
use ApiPlatform\Toon\Serializer\ToonJsonLdItemNormalizer;
use Illuminate\Config\Repository as ConfigRepository;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Routing\Router;
Expand Down Expand Up @@ -963,6 +970,16 @@ public function register(): void
$list->insert($app->make(JsonApiErrorNormalizer::class), -790);
$list->insert($app->make(JsonApiObjectNormalizer::class), -995);

$list->insert(new ToonHydraCollectionNormalizer($app->make(HydraCollectionNormalizer::class)), -880);
$list->insert(new ToonHydraEntrypointNormalizer(
$app->make(HydraEntrypointNormalizer::class),
$app->make(ResourceMetadataCollectionFactoryInterface::class)
), -880);
$list->insert(new ToonJsonApiCollectionNormalizer($app->make(JsonApiCollectionNormalizer::class)), -880);
$list->insert(new ToonJsonApiEntrypointNormalizer($app->make(JsonApiEntrypointNormalizer::class)), -880);
$list->insert(new ToonJsonApiItemNormalizer($app->make(JsonApiItemNormalizer::class)), -880);
$list->insert(new ToonJsonLdItemNormalizer($app->make(JsonLdItemNormalizer::class)), -880);

if (interface_exists(FieldsBuilderEnumInterface::class)) {
$list->insert($app->make(GraphQlItemNormalizer::class), -890);
$list->insert($app->make(GraphQlObjectNormalizer::class), -995);
Expand All @@ -987,6 +1004,8 @@ public function register(): void
return new Serializer(
iterator_to_array($app->make('api_platform_normalizer_list')),
[
// ToonEncoder must come first to handle Toon-encoded formats before JSON encoder
new ToonEncoder(),
new JsonEncoder('json'),
$app->make(JsonEncoder::class),
new JsonEncoder('jsonopenapi'),
Expand Down
202 changes: 202 additions & 0 deletions src/Laravel/Tests/ToonTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
<?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 HelgeSverre\Toon\Toon;
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\Author;
use Workbench\App\Models\Book;
use Workbench\Database\Factories\AuthorFactory;
use Workbench\Database\Factories\BookFactory;

class ToonTest extends TestCase
{
use ApiTestAssertionsTrait;
use RefreshDatabase;
use WithWorkbench;

/**
* @param Application $app
*/
protected function defineEnvironment($app): void
{
tap($app['config'], static function (Repository $config): void {
// Add Toon format as separate format (uses JSON-LD normalizers with Toon encoder)
$formats = $config->get('api-platform.formats', []);
$formats['toon'] = ['text/ld+toon'];
$formats['jsonld_toon'] = ['text/ld+toon']; // Explicitly add jsonld_toon
$formats['hydra_toon'] = ['text/ld+toon']; // Explicitly add hydra_toon
$formats['jsonapi_toon'] = ['text/vnd.api+toon']; // Explicitly add jsonapi_toon
$config->set('api-platform.formats', $formats);

$patchFormats = $config->get('api-platform.patch_formats', []);
$patchFormats['toon'] = ['text/ld+toon'];
$patchFormats['jsonld_toon'] = ['text/ld+toon']; // Explicitly add jsonld_toon
$patchFormats['hydra_toon'] = ['text/ld+toon']; // Explicitly add hydra_toon
$patchFormats['jsonapi_toon'] = ['text/vnd.api+toon']; // Explicitly add jsonapi_toon
$config->set('api-platform.patch_formats', $patchFormats);

$docsFormats = $config->get('api-platform.docs_formats', []);
$docsFormats['toon'] = ['text/ld+toon'];
$docsFormats['jsonld_toon'] = ['text/ld+toon']; // Explicitly add jsonld_toon
$docsFormats['hydra_toon'] = ['text/ld+toon']; // Explicitly add hydra_toon
$docsFormats['jsonapi_toon'] = ['text/vnd.api+toon']; // Explicitly add jsonapi_toon
$config->set('api-platform.docs_formats', $docsFormats);

$config->set('app.debug', true);
});
}

public function testGetEntrypoint(): void
{
$response = $this->get('/api/', ['accept' => ['text/ld+toon']]);
$response->assertStatus(200);
$response->assertHeader('content-type', 'text/ld+toon; charset=utf-8');

$content = $response->getContent();

// Decode the Toon content to check structure
$decoded = \HelgeSverre\Toon\Toon::decode($content);

$this->assertIsArray($decoded);
// Laravel entrypoint might have different structure, just check it's an array with resources
$this->assertNotEmpty($decoded);
// The response should contain book-related information
$contentLower = strtolower($content);
$this->assertTrue(
str_contains($contentLower, 'book') || str_contains($contentLower, 'api'),
'Entrypoint should contain resource information'
);
}

public function testGetCollection(): void
{
BookFactory::new()->has(AuthorFactory::new())->count(10)->create();
$response = $this->get('/api/books', ['accept' => 'text/ld+toon']);
$response->assertStatus(200);
$response->assertHeader('content-type', 'text/ld+toon; charset=utf-8');

$content = $response->getContent();

// Decode to verify structure
$decoded = \HelgeSverre\Toon\Toon::decode($content);

// Check for collection structure (Laravel doesn't use hydra prefix)
$this->assertIsArray($decoded);

// Check if it's a proper collection or a plain array
if (isset($decoded['@id'])) {
// Full collection structure
$this->assertArrayHasKey('totalItems', $decoded);
$this->assertEquals(10, $decoded['totalItems']);
$this->assertArrayHasKey('member', $decoded);
$this->assertCount(5, $decoded['member']); // Default page size
} else {
// Plain array of items
$this->assertIsArray($decoded);
$this->assertNotEmpty($decoded);
// Just verify we got items back
$this->assertGreaterThan(0, count($decoded));
}
}

public function testGetBook(): void
{
BookFactory::new()->has(AuthorFactory::new())->count(10)->create();
$book = Book::first();
$iri = $this->getIriFromResource($book);
$response = $this->get($iri, ['accept' => ['text/ld+toon']]);
$response->assertStatus(200);
$response->assertHeader('content-type', 'text/ld+toon; charset=utf-8');

$content = $response->getContent();

$this->assertStringContainsString('id:', $content);
$this->assertStringContainsString('name: '.$book->name, $content); // @phpstan-ignore-line
// ISBN may be quoted in output
$this->assertStringContainsString($book->isbn, $content); // @phpstan-ignore-line
}

public function testCreateBook(): void
{
AuthorFactory::new()->count(10)->create();
$author = Author::find(1);

$isbn = fake()->isbn13();

$response = $this->postJson(
'/api/books',
[
'name' => 'The Pragmatic Programmer',
'isbn' => $isbn,
'publicationDate' => fake()->optional()->date(),
'author' => $this->getIriFromResource($author),
],
[
'accept' => 'text/ld+toon',
'content-type' => 'application/ld+json',
]
);

$response->assertStatus(201);
$response->assertHeader('content-type', 'text/ld+toon; charset=utf-8');

$content = $response->getContent();

$this->assertStringContainsString('name: The Pragmatic Programmer', $content);
$this->assertStringContainsString('id:', $content);
}

public function testUpdateBook(): void
{
BookFactory::new()->has(AuthorFactory::new())->count(10)->create();
$book = Book::first();
$iri = $this->getIriFromResource($book);

$response = $this->patchJson(
$iri,
[
'name' => 'Updated Title',
],
[
'accept' => 'text/ld+toon',
'content-type' => 'application/merge-patch+json',
]
);

$response->assertStatus(200);
$response->assertHeader('content-type', 'text/ld+toon; charset=utf-8');

$content = $response->getContent();

$this->assertStringContainsString('name: Updated Title', $content);
// ISBN may be quoted in output
$this->assertStringContainsString($book->isbn, $content); // @phpstan-ignore-line unchanged
}

public function testDeleteBook(): void
{
BookFactory::new()->has(AuthorFactory::new())->count(10)->create();
$book = Book::first();
$iri = $this->getIriFromResource($book);
$response = $this->delete($iri, headers: ['accept' => 'text/ld+toon']);
$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 @@ -38,6 +38,7 @@
"api-platform/openapi": "^4.2",
"api-platform/serializer": "^4.2.4",
"api-platform/state": "^4.2.4",
"api-platform/toon": "^4.2",
"illuminate/config": "^11.0 || ^12.0",
"illuminate/container": "^11.0 || ^12.0",
"illuminate/contracts": "^11.0 || ^12.0",
Expand Down
14 changes: 14 additions & 0 deletions src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ public function load(array $configs, ContainerBuilder $container): void
$this->registerJsonApiConfiguration($formats, $loader, $config);
$this->registerJsonLdHydraConfiguration($container, $formats, $loader, $config);
$this->registerJsonHalConfiguration($formats, $loader);
$this->registerToonConfiguration($formats, $loader);
$this->registerJsonProblemConfiguration($errorFormats, $loader);
$this->registerGraphQlConfiguration($container, $config, $loader);
$this->registerCacheConfiguration($container);
Expand Down Expand Up @@ -723,6 +724,19 @@ private function registerJsonHalConfiguration(array $formats, PhpFileLoader $loa
$loader->load('hal.php');
}

private function registerToonConfiguration(array $formats, PhpFileLoader $loader): void
{
if (!isset($formats['toon'])) {
return;
}

if (!InstalledVersions::isInstalled('api-platform/toon')) {
throw new \LogicException('Toon support cannot be enabled as the Toon component is not installed. Try running "composer require api-platform/toon".');
}

$loader->load('toon.php');
}

private function registerJsonProblemConfiguration(array $errorFormats, PhpFileLoader $loader): void
{
if (!isset($errorFormats['jsonproblem'])) {
Expand Down
58 changes: 58 additions & 0 deletions src/Symfony/Bundle/Resources/config/toon.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?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 Symfony\Component\DependencyInjection\Loader\Configurator;

return static function (ContainerConfigurator $container) {
$services = $container->services();

// Toon encoder - can be used with any representation format (JSON-LD, JSON:API, HAL, Hydra)
// Priority: 10 to ensure it's checked before JsonEncoder for jsonld format
$services->set('api_platform.toon.encoder', 'ApiPlatform\Toon\Serializer\ToonEncoder')
->tag('serializer.encoder', ['priority' => 10]);

// Toon Normalizers

$services->set('api_platform.toon.normalizer.hydra.collection', 'ApiPlatform\Toon\Serializer\ToonHydraCollectionNormalizer')
->decorate('api_platform.hydra.normalizer.collection', null, 0)
->args([
service('.inner'),
])
->tag('serializer.normalizer', ['priority' => 8]);

$services->set('api_platform.toon.normalizer.hydra.entrypoint', 'ApiPlatform\Toon\Serializer\ToonHydraEntrypointNormalizer')
->decorate('api_platform.hydra.normalizer.entrypoint')
->args([
service('.inner'),
service('api_platform.metadata.resource.metadata_collection_factory')
])
->tag('serializer.normalizer', ['priority' => 8]);

$services->set('api_platform.toon.normalizer.jsonapi.collection', 'ApiPlatform\Toon\Serializer\ToonJsonApiCollectionNormalizer')
->decorate('api_platform.jsonapi.normalizer.collection', null, 0)
->args([service('.inner')])
->tag('serializer.normalizer', ['priority' => 8]);

$services->set('api_platform.toon.normalizer.jsonapi.entrypoint', 'ApiPlatform\Toon\Serializer\ToonJsonApiEntrypointNormalizer')
->decorate('api_platform.jsonapi.normalizer.entrypoint')
->args([service('.inner')])
->tag('serializer.normalizer', ['priority' => 8]);
$services->set('api_platform.toon.normalizer.jsonapi.item', 'ApiPlatform\Toon\Serializer\ToonJsonApiItemNormalizer')
->decorate('api_platform.jsonapi.normalizer.item')
->args([service('.inner')])
->tag('serializer.normalizer', ['priority' => 8]);
$services->set('api_platform.toon.normalizer.jsonld.item', 'ApiPlatform\Toon\Serializer\ToonJsonLdItemNormalizer')
->decorate('api_platform.jsonld.normalizer.item')
->args([service('.inner')])
->tag('serializer.normalizer', ['priority' => 8]);
};
Loading
Loading