Installation:
composer require nocarrier/hal
Add to composer.json if not using autoload globally:
"autoload": {
"psr-4": {
"App\\": "app/",
"Nocarrier\\": "vendor/nocarrier/hal/src/"
}
}
Run composer dump-autoload.
First Use Case: Create a simple HAL resource in a Laravel controller:
use Nocarrier\Hal;
public function showOrder(Order $order)
{
$resource = new Hal('/api/orders/' . $order->id, [
'id' => $order->id,
'amount' => $order->amount,
'status' => $order->status,
]);
$resource->addLink('self', '/api/orders/' . $order->id);
$resource->addLink('customer', '/api/customers/' . $order->user_id);
return response()->json($resource->asJson());
}
Key Starting Points:
Hal class constructor takes URI and data array.addLink() for relationships (e.g., self, collection, up).addResource() for nested HAL documents.asJson() or asXml().Use HAL to standardize API responses with hypermedia controls:
public function index()
{
$collection = new Hal('/api/orders', [], 'orders');
foreach ($orders as $order) {
$resource = new Hal('/api/orders/' . $order->id, [
'id' => $order->id,
'amount' => $order->amount,
]);
$resource->addLink('self', '/api/orders/' . $order->id);
$collection->addResource('order', $resource);
}
return response()->json($collection->asJson());
}
Generate links dynamically in controllers or services:
$resource->addLink('edit', route('admin.orders.edit', $order->id));
$resource->addLink('delete', route('admin.orders.destroy', $order->id), [
'method' => 'DELETE',
'title' => 'Delete Order',
]);
Fetch and embed related models (e.g., customer for an order):
$orderResource = new Hal('/api/orders/' . $order->id, [...]);
$customerResource = new Hal('/api/customers/' . $order->user_id, [
'name' => $order->user->name,
]);
$orderResource->addResource('customer', $customerResource);
Create middleware to wrap responses in HAL format:
public function handle($request, Closure $next)
{
$response = $next($request);
if ($response->isJson()) {
$data = $response->json();
$hal = new Hal($request->url(), $data);
$response->setContent($hal->asJson());
}
return $response;
}
Resource ClassesCombine with Laravel's JsonResource for cleaner code:
public function toArray($request)
{
return [
'id' => $this->id,
'amount' => $this->amount,
'_links' => [
'self' => ['href' => route('api.orders.show', $this)],
],
];
}
Then convert to HAL in the controller:
$resource = new Hal('/api/orders/' . $order->id, $order->toArray($request));
Bind HAL to the container for dependency injection:
public function register()
{
$this->app->bind(Hal::class, function () {
return new Hal('/');
});
}
Use HAL to include versioned links:
$resource->addLink('self', '/v1/orders/' . $order->id);
$resource->addLink('latest', '/v2/orders/' . $order->id);
Cache HAL-formatted responses in Laravel:
return Cache::remember("hal.order.{$order->id}", now()->addHours(1), function () use ($order) {
return response()->json((new Hal(...))->asJson());
});
Test HAL responses with PHPUnit:
public function testHalResponse()
{
$response = $this->get('/api/orders/1');
$hal = json_decode($response->getContent(), true);
$this->assertArrayHasKey('_links', $hal);
$this->assertArrayHasKey('self', $hal['_links']);
}
/api/orders/1). Relative paths (e.g., ../orders/1) may break links.url() or route() helpers to generate absolute paths:
$resource->addLink('self', url('/api/orders/' . $order->id));
addLink() accepts href and title as separate parameters, but the method signature might be confusing:
// Correct:
$resource->addLink('self', '/url', ['title' => 'Order Details']);
// Incorrect (will throw error):
$resource->addLink('self', ['href' => '/url', 'title' => 'Order Details']);
addResource('customer', ...)), the last one wins.asXml() method may not handle all edge cases (e.g., special characters in data).htmlspecialchars():
$data['description'] = htmlspecialchars($order->description);
$collection->addResource('order', $resource); // Only add current page's orders
Use online validators like HAL Explorer to check your output:
// Dump raw HAL for debugging:
dd($resource->asJson());
Ensure _links are always included in responses:
if (!isset($hal['_links'])) {
throw new \RuntimeException('HAL document missing _links section.');
}
Log HAL responses for debugging:
\Log::debug('HAL Response', [
'uri' => $resource->getUri(),
'data' => $resource->asJson(),
]);
Extend HAL to support custom link relations (e.g., api:update):
$resource->addLink('api:update', '/api/orders/' . $order->id, [
'method' => 'PATCH',
'title' => 'Update Order',
]);
Include custom metadata in the _meta section:
$resource->setMeta(['created_at' => $order->created_at->toIsoString()]);
Extend the Hal class to customize JSON/XML output:
class CustomHal extends Hal
{
public function asJson()
{
$json = parent::asJson();
// Modify $json here (e.g., add timestamps)
return $json;
}
}
Use HAL to expose policy-based links:
if ($this->authorize('update', $order)) {
$resource->addLink('update', route('api.orders.update', $order));
}
Combine HAL with CORS middleware:
$response = response()->json($
How can I help you explore Laravel packages today?