gointegro/hateoas-bundle
GoIntegro Hateoas Bundle brings HATEOAS to Symfony REST APIs: a JSON:API-style response format with pagination, sorting, filtering, sparse fieldsets, and embedded related resources, plus annotations and helpers to expose resources and links with minimal configuration.
First, calibrate your expectations: this is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no Eloquent integration. If you are on Laravel, you will not be composer require-ing this into app/; you will either be (a) reading it as a reference implementation for JSON:API/HATEOAS semantics, or (b) working in a Symfony codebase that already has it installed. Everything below assumes (b).
Minimal adoption path:
composer require gointegro/hateoas-bundle
Register the bundle in AppKernel (new GoIntegro\Bundle\HateoasBundle\GoIntegroHateoasBundle()), then map entities to resources:
# app/config/config.yml
go_integro_hateoas:
mapping:
resources:
'Acme\BlogBundle\Entity\Post': 'Acme\BlogBundle\Resource\PostResource'
Import the bundle routing so the default resource controller is reachable, and annotate the entity:
use GoIntegro\Hateoas\JsonApi\Annotation as HATEOAS;
/**
* @HATEOAS\Resource(type="posts", controller="AcmeBlogBundle:Post")
*/
class Post
{
/** @HATEOAS\Attribute */
private $title;
/** @HATEOAS\Relationship("author") */
private $author;
}
Your first use case should be the smallest possible one: one entity, one attribute, one relationship, GET /posts/1. You want to see this shape before going further, because the entire value of the package is in the document envelope:
{
"data": {
"type": "posts",
"id": "1",
"attributes": { "title": "Hello" },
"relationships": { "author": { "links": { "related": "/posts/1/author" } } },
"links": { "self": "/posts/1" }
}
}
Where to look first, in order: DependencyInjection/Configuration.php (the real config surface — the README lags the code), Controller/ResourceController.php (the default CRUD actions you'll extend or copy), JsonApi/ResourceManager.php and JsonApi/ResourceEntity.php (the core abstractions), then Tests/ for executable examples of relationships, pagination and security.
Annotation-first, resource-class-second. Most day-to-day work is annotation-driven: @Attribute, @Relationship, @Link, @ResourceCollection on entity properties. Reach for an explicit ResourceEntity subclass only when you need computed attributes, links whose logic doesn't fit an annotation, or a resource that doesn't map 1:1 to a Doctrine entity. Mixing the two styles in one domain is the fastest route to confusion — pick one per entity.
Controller pattern. For standard CRUD, extend the bundle's resource controller and override only what differs. For custom endpoints, inject ResourceManager and let it build the document:
public function getAction($id, ResourceManager $rm)
{
return $rm->createResponse($rm->get('posts', $id));
}
The manager resolves the entity, applies the resource mapping, runs the serializer, and sets the application/vnd.api+json content type. Keeping this call in your controller (rather than hand-rolling arrays) is what preserves compound documents, include, and sparse fieldsets for free.
Relationships and include. Declare relationships with @Relationship and expose collections with @ResourceCollection. Clients then drive traversal with ?include=author,comments.author. The bundle handles the compound-document assembly and the included array — you do not build it yourself. This is the single biggest time-saver in the package.
Query-parameter conventions. Pagination, sorting and field filtering all flow through standard JSON:API query params (page[number], page[size], sort, fields[posts]). Wire the pagination defaults in config rather than per-controller so all endpoints behave consistently.
Security belongs in links, not in controllers. The bundle's security integration evaluates Symfony's isGranted when generating links and relationships. The idiom is: if a user can't POST /posts, the create link is simply absent from the document. Prefer this over returning 403s from actions — it lets clients discover capabilities instead of probing for them.
Caching. Link generation depends on the current user's permissions, so any cache key must include the user or role. The bundle exposes a cache layer; use it per-resource, and invalidate on write.
The Laravel mismatch is real and non-negotiable. No Eloquent, no artisan, no config publishing. It expects Doctrine ORM entities, Symfony's DI container, JMSSerializer and typically FOSRestBundle. If you were hoping to drop this into a Laravel API, budget for a rewrite rather than a port.
Check version constraints before anything else. This bundle targets the Symfony 2.x/3.x, PHP 5.6/7.x era and JSON:API 1.0. Open composer.json and verify the Symfony and PHP ceilings against your project before you design around it.
Low maintenance signal. 68 stars, zero dependents on the tracked index, and a small contributor base. Treat it as vendored infrastructure: expect to read the source, and be prepared to fork if a Symfony upgrade blocks you. Pin the version.
Config key names drift between releases. mapping.resources vs resources, pagination sub-keys, cache settings — verify against DependencyInjection/Configuration.php, not the README. A silently ignored config block is the most common "it does nothing" cause.
Missing annotations fail silently. An entity with no @Attribute/@Relationship produces a valid but empty resource — no exception, just a document with no fields. When an attribute doesn't appear, first confirm the annotation is on the property (not the getter) and that the annotation namespace was imported.
N+1 on include. Serializing relationships lazily is the classic performance cliff. Use fetch joins or Doctrine hints on the collection query for the relationships you know are commonly included, and cap include depth to prevent a client from requesting a cyclic graph.
Cache keys must include the user. Because links encode authorization, caching a document built for an admin and serving it to a guest leaks capabilities. Key on resource + user/role + include/fields params.
Write requests expect JSON:API documents. POST/PATCH bodies are {"data": {"type": "...", "attributes": {...}}}, not flat JSON. Clients that send plain payloads get confusing 400s. Document this loudly for frontend consumers.
Debugging tip: dump the ResourceManager output before serialization to separate mapping problems from serializer problems. If the resource looks right but the JSON doesn't, the issue is in JMSSerializer metadata or FOSRestBundle content negotiation, not in your annotations.
Extension points worth knowing: subclass ResourceEntity to override getLinks(); decorate ResourceManager in the container to add cross-cutting behavior (tenant scoping, audit fields); use a compiler pass to auto-register resource mappings from a directory rather than hand-listing every entity in YAML.
How can I help you explore Laravel packages today?