Installation
composer require api-platform/hal
Add the ApiPlatform\Hal\Serializer\ContextBuilder to your serializer context builders in config/services.php:
'serializer' => [
'context_builders' => [
// ...
ApiPlatform\Hal\Serializer\ContextBuilder::class,
],
],
First Use Case Use the HAL serializer in a controller or API resource to return HAL-formatted responses:
use ApiPlatform\Hal\Serializer\ContextBuilder;
use Symfony\Component\Serializer\SerializerInterface;
class PostController extends AbstractController
{
public function __invoke(SerializerInterface $serializer, ContextBuilder $contextBuilder)
{
$data = ['id' => 1, 'title' => 'Hello HAL'];
$context = $contextBuilder->createSerializationContext([]);
return new JsonResponse(
$serializer->serialize($data, 'jsonhal', $context)
);
}
}
Where to Look First
ApiPlatform\Hal\Serializer\ContextBuilder for context configuration.ApiPlatform\Hal\Serializer\Normalizer\HalNormalizer for customization.Embedding Resources
Use the embedded key in the context to include related resources:
$context = $contextBuilder->createSerializationContext([
'groups' => ['post:read'],
'embedded' => ['author', 'comments'],
]);
Custom Links
Add custom links via the links key in the context:
$context = $contextBuilder->createSerializationContext([
'links' => [
'self' => ['href' => '/api/posts/1'],
'custom' => ['href' => '/api/posts/1/related'],
],
]);
Partial Serialization Leverage Symfony’s serializer groups for partial data:
// In your entity
#[Groups(['post:read'])]
private $title;
#[Groups(['post:write'])]
private $content;
// In the context
$context = $contextBuilder->createSerializationContext(['groups' => ['post:read']]);
Integration with API Platform
If using API Platform, the HAL format is enabled by default for jsonhal format. Customize via:
# config/api_platform/resources.yaml
resources:
App\Entity\Post:
collectionOperations:
get:
normalization_context:
groups: ['post:read']
embedded: ['author']
Request-Response Cycle
ContextBuilder to dynamically adjust serialization based on request attributes (e.g., Accept header).public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
$contextBuilder = $this->container->get(ContextBuilder::class);
$context = $contextBuilder->createSerializationContext([
'format' => $request->getRequestFormat(),
]);
$request->attributes->set('serialization_context', $context);
}
Testing
Mock the ContextBuilder and SerializerInterface in tests:
$contextBuilder = $this->createMock(ContextBuilder::class);
$contextBuilder->method('createSerializationContext')->willReturn([]);
$serializer = $this->createMock(SerializerInterface::class);
$serializer->expects($this->once())
->method('serialize')
->with($data, 'jsonhal', $context);
Context Overrides
ContextBuilder merges contexts. Ensure explicit overrides in your code don’t conflict with defaults:
// Bad: Overrides all links
$context = $contextBuilder->createSerializationContext(['links' => []]);
// Good: Preserves defaults while adding custom links
$context = $contextBuilder->createSerializationContext([
'links' => ['custom' => ['href' => '/custom']],
]);
Circular References
Post ↔ Author). Use ignore_circular_references:
$context = $contextBuilder->createSerializationContext([
'ignore_circular_references' => true,
]);
Performance
Inspect Context Dump the context to debug serialization issues:
$context = $contextBuilder->createSerializationContext([]);
dump($context); // Inspect keys like 'groups', 'embedded', etc.
Normalizer Order
If HAL output is unexpected, check the normalizer order in config/packages/api_platform.yaml:
api_platform:
formats:
jsonhal:
mime_types: ['application/hal+json']
serializer_normalizer: 'api_platform.hal_serializer.normalizer.hal'
Custom Normalizers
Extend HalNormalizer for custom logic:
use ApiPlatform\Hal\Serializer\Normalizer\HalNormalizer;
class CustomHalNormalizer extends HalNormalizer
{
public function normalize($object, $format, array $context = [])
{
$data = parent::normalize($object, $format, $context);
$data['_custom'] = 'value'; // Add custom key
return $data;
}
}
Register it in config/services.yaml:
services:
App\Serializer\Normalizer\CustomHalNormalizer:
tags: [serializer.normalizer]
arguments:
- 'jsonhal'
decorates: 'api_platform.hal_serializer.normalizer.hal'
Dynamic Links Create a custom context builder to inject dynamic links:
use ApiPlatform\Hal\Serializer\ContextBuilder as BaseContextBuilder;
class DynamicContextBuilder extends BaseContextBuilder
{
public function createSerializationContext(array $context = [])
{
$context['links']['dynamic'] = [
'href' => route('dynamic.route', ['id' => 123]),
];
return parent::createSerializationContext($context);
}
}
Custom HAL Metadata
Extend the HAL structure by overriding the normalizer’s normalize method (as shown above) or by using a custom metadata factory.
Integration with API Platform Override the default HAL format in API Platform’s configuration to add custom logic:
api_platform:
formats:
jsonhal:
mime_types: ['application/hal+json']
serializer_normalizer: 'App\Serializer\Normalizer\CustomHalNormalizer'
How can I help you explore Laravel packages today?