programmingarehard/resource-bundle
ProgrammingAreHard ResourceBundle is a Symfony bundle for resource-oriented development. It adds conventions for defining resources and routing RESTful CRUD actions, helping you keep controllers thin and expose consistent APIs with less boilerplate.
Install the bundle through Composer and register it in config/bundles.php (or app/AppKernel.php on older Symfony versions):
composer require programmingarehard/resource-bundle
// config/bundles.php
return [
// ...
ProgrammingAreHard\ResourceBundle\ResourceBundle::class => ['all' => true],
];
Because the bundle is small (33 stars, single maintainer) and has no dependents, treat the repository itself as the primary documentation. Read these first, in order:
README.md — the only canonical source for config keys and required services.DependencyInjection/ — tells you exactly which services, aliases and parameters are registered and whether any are mandatory.Controller/ (or equivalent) — the abstract base classes you are expected to extend; this is where the day-to-day API lives.Tests/ — the most reliable spec for expected request/response shapes and edge cases.The first end-to-end use case to try is the smallest possible CRUD resource: a single entity, one abstract controller subclass, one route, and a JSON GET by id. Confirm the response is shaped the way you expect (Content-Type, envelope, serialization groups) before wiring up create/update, because the bundle's defaults around serialization and status codes are easier to accept early than to change after you have ten controllers inheriting them.
If you are coming from Laravel, map the mental model directly: this bundle plays the role of Route::apiResource plus an ApiController base class plus a Resource/JsonResource transformer, but glued together with Symfony's service container and event dispatcher instead of facades. Everything is injected, not resolved from a global helper.
Inheritance-first controllers. The dominant pattern in this kind of bundle is to extend an abstract controller that already implements index, show, store, update and destroy. Your subclasses then declare which entity/repository/form they operate on and leave the HTTP plumbing (status codes, headers, serialization) to the base class. Follow this pattern rather than composing services ad hoc; if you fight it, you lose the bundle's main value.
Resource-oriented routing. Each concrete controller maps one-to-one to a URL collection. Use Symfony's @Route annotations or routes.yaml with the standard REST verbs, and keep controller method names aligned with the abstract methods so route generation stays predictable.
Form-driven deserialization. Expect request bodies to be hydrated through Symfony Forms (or a form-like resolver). Validate with standard constraint annotations on the entity/DTO, then let the base controller convert validation failures into a 422. This keeps validation declarative and reusable across web and API.
Serialization via groups. Configure serialization groups on the entity and let the bundle's view/listener produce the response. Define separate list, detail, and write groups rather than exposing everything — this is the single biggest lever you have over API shape without touching controllers.
Pagination and filtering. Most bundles in this space expose a paginator helper and accept page, limit, and simple filters as query parameters. Encapsulate custom filters in the repository, not controllers, so both the paginator and any future GraphQL/CLI endpoint reuse them.
Exception mapping. Configure a listener or exception map that converts domain exceptions (NotFound, ValidationFailed, Forbidden) into HTTP responses. Centralizing this means controllers can throw and stay clean, and error payloads remain consistent across resources.
Cross-cutting concerns via event subscribers. Use kernel events (kernel.request, kernel.view, kernel.exception) rather than middleware-style base classes for auth, rate limiting, and response headers. This mirrors Laravel's middleware conceptually but composes better with Symfony's container.
Integration with the serializer. If the project already uses JMS Serializer or Symfony Serializer, verify which one the bundle expects and configure one, not both. Mixed serializers are a common source of "empty JSON body" bugs.
Sparse documentation is the main risk. With 33 stars and no dependents, you will hit undocumented behavior. Budget time to read the source. Keep notes in your own docs/api-bundle.md so the next developer on the team does not rediscover the same quirks.
Symfony version compatibility. Bundles this size often target a specific Symfony major (usually 4/5). Check composer.json constraints before committing. If you are on Symfony 6+ or 7+, expect to fork and patch the bundle or override service definitions in services.yaml.
Service overrides are your escape hatch. If a base controller's behavior is wrong for one resource, override the specific method in your subclass rather than editing vendor code, and if the bundle is missing an extension point, register a compiler pass or a decorating service in your app.
Serialization context leakage. A detail group that accidentally includes a relation with another detail group can cascade into enormous payloads or circular-reference errors. Always test the exact JSON shape with a feature test (assert response keys, not just status codes).
Status code defaults. REST bundles frequently return 200 where the spec expects 201 or 204. Check store and destroy explicitly in tests; if you cannot configure this, override in the subclass and document the deviation.
Route collisions. Auto-generated route names from controllers with similar names will collide silently if using annotations. Prefix route names per resource (api_products_*) to keep debug:router readable and prevent 404s after refactors.
Debug toolkit. Use bin/console debug:router, debug:container --tag=<bundle-tag>, and the Symfony profiler's "Request / Response" panel to see exactly which listener is shaping the payload. When a response is unexpectedly empty, check kernel.view listeners first — the bundle's view handler likely did not fire because no controller returned a non-Response value.
Error handling. Wrap bundle exceptions in your own app-level exception map if the bundle only handles a narrow set. A NotFoundHttpException slipping through as a 500 due to a missing catch is a classic first-week bug.
Testing discipline. Because the bundle controls both input (form deserialization) and output (serialization), write one functional test per resource covering: list with pagination, show by id, create valid, create invalid, update, delete. These tests double as documentation of the bundle's actual contract, which the README likely does not provide.
Fork readiness. Given the low adoption, plan for the possibility that you will need to vendor the bundle into src/ within a year. Keep your subclass code thin and put business logic in your own services so a future replacement of the controller layer is a mechanical change.
How can I help you explore Laravel packages today?