alexfigures/symfony-jsonapi-bundle
Installation:
composer require alexfigures/symfony-jsonapi-bundle
Add to config/bundles.php:
return [
// ...
AlexFigures\JsonApiBundle\JsonApiBundle::class => ['all' => true],
];
Basic Controller:
use AlexFigures\JsonApiBundle\Controller\JsonApiController;
use Symfony\Component\HttpFoundation\Response;
class ArticleController extends JsonApiController
{
public function index(): Response
{
return $this->collection('articles', ArticleResource::class);
}
}
Resource Class:
use AlexFigures\JsonApiBundle\Resource\ResourceInterface;
class ArticleResource implements ResourceInterface
{
public function getType(): string { return 'articles'; }
public function getId(): string { return $this->article->id; }
public function getAttributes(): array { return $this->article->toArray(); }
}
Routing:
# config/routes.yaml
api_articles:
path: /api/articles
controller: App\Controller\ArticleController::index
methods: [GET]
Create a simple GET endpoint returning paginated JSON:API-compliant data:
// src/Controller/ArticleController.php
public function index(): Response
{
$articles = ArticleRepository::findAllWithPagination();
return $this->collection('articles', ArticleResource::class, $articles);
}
Resource-Based Routing:
// Automatically maps to /api/articles/{id}
public function show(string $id): Response
{
return $this->item('articles', ArticleResource::class, $id);
}
Sparse Fieldsets:
// Only returns requested fields
public function index(): Response
{
return $this->collection(
'articles',
ArticleResource::class,
ArticleRepository::findAll(),
['fields[articles]' => 'title,body']
);
}
Relationship Handling:
// To-many relationship
public function comments(string $id): Response
{
return $this->relationship(
'articles',
'comments',
ArticleResource::class,
$id,
CommentResource::class,
ArticleRepository::find($id)->comments
);
}
Doctrine Integration:
use AlexFigures\JsonApiBundle\Resource\Doctrine\DoctrineResource;
class ArticleResource extends DoctrineResource
{
protected static string $entityClass = Article::class;
protected static array $fields = ['title', 'body', 'publishedAt'];
}
Custom Error Handling:
use AlexFigures\JsonApiBundle\Exception\JsonApiHttpException;
try {
return $this->item(...);
} catch (EntityNotFoundException $e) {
throw new JsonApiHttpException(404, 'Article not found', [
'source' => ['pointer' => '/data/attributes/id']
]);
}
Event Listeners:
use AlexFigures\JsonApiBundle\Event\ResourceEvent;
public function onResourceBuild(ResourceEvent $event): void
{
if ($event->getResource() instanceof ArticleResource) {
$event->getData()->setAttribute('author', $event->getResource()->getAuthor());
}
}
Pagination:
// Page-based pagination
return $this->collection(
'articles',
ArticleResource::class,
ArticleRepository::findAllPaginated(),
[],
['page[size]' => 20, 'page[number]' => 1]
);
Caching:
# config/packages/cache.yaml
framework:
cache:
app.jsonapi: ~
use Symfony\Component\HttpFoundation\Response;
public function index(): Response
{
return $this->collection(
'articles',
ArticleResource::class,
ArticleRepository::findAll(),
[],
[],
['cache' => ['ttl' => 300]]
);
}
Field Name Validation:
id, type) cannot be used as attributes unless explicitly allowed@, spaces) will trigger 400 errorspublic function getAttributes(): array
{
$attributes = $this->article->toArray();
return array_filter($attributes, fn($k) => !in_array($k, ['id', 'type']), ARRAY_FILTER_USE_KEY);
}
Relationship Data Structure:
data array even for empty relationships:
return [
'data' => [] // Not null or omitted
];
Pagination Links:
links in paginated responses will fail conformancereturn $this->collection(
'articles',
ArticleResource::class,
$articles,
[],
[],
['pagination' => true]
);
Enable Debug Mode:
# config/packages/dev/jsonapi.yaml
jsonapi:
debug: true
Validation Errors:
errors array in responses (400 status)jsonapi:validate command:
php bin/console jsonapi:validate path/to/your/resource.json
Performance Profiling:
php bin/console debug:autowiring AlexFigures\JsonApiBundle
Custom Resource Builders:
use AlexFigures\JsonApiBundle\Resource\ResourceBuilderInterface;
class CustomResourceBuilder implements ResourceBuilderInterface
{
public function build(array $data, string $type, string $id): ResourceInterface
{
return new CustomResource($data, $type, $id);
}
}
Register in config:
jsonapi:
resource_builder: App\Service\CustomResourceBuilder
Hook System:
use AlexFigures\JsonApiBundle\Event\ResourceEvent;
// Subscribe to resource building
$dispatcher->addListener(ResourceEvent::RESOURCE_BUILD, [$this, 'onResourceBuild']);
// Subscribe to response building
$dispatcher->addListener(ResourceEvent::RESPONSE_BUILD, [$this, 'onResponseBuild']);
Custom Error Providers:
use AlexFigures\JsonApiBundle\Error\ErrorProviderInterface;
class CustomErrorProvider implements ErrorProviderInterface
{
public function getError(int $status, string $title, array $meta = []): array
{
return [
'errors' => [
[
'status' => $status,
'title' => $title,
'meta' => [
'custom' => 'value',
...$meta
]
]
]
];
}
}
Configure in services:
services:
App\Error\CustomErrorProvider:
tags: ['jsonapi.error_provider']
Strict Mode:
jsonapi:
strict: true # Enforces all MUST requirements
Profile Support:
jsonapi:
profiles:
- 'https://example.com/profiles/article.v1'
- 'https://jsonapi.org/format'
Surrogate Keys:
jsonapi:
surrogate_keys:
articles: 'articles/{id}'
Test Helpers:
use AlexFigures\JsonApiBundle\Test\JsonApiTestCase;
class ArticleTest extends JsonApiTestCase
{
public function testArticleResource()
{
$response = $this->client->request('GET', '/api/articles/1');
$this->assertJsonApiResponse($response, 200);
$this->assertJsonApiDocument($response, [
'data' => [
'type' => 'articles',
'id' => '1',
'attributes' => [
'title' => 'Test Article',
'body' => 'Content...'
]
]
]);
}
}
Snapshot Testing:
use AlexFigures\JsonApiBundle\Test\SnapshotTestTrait;
class ConformanceTest extends TestCase
{
How can I help you explore Laravel packages today?