happyr/api-bundle
HappyR API Bundle is a PHP bundle that provides a reusable foundation for building APIs, helping you organize endpoints and standardize responses so you can ship consistent, maintainable API services faster.
Before installing, do a five-minute reality check: the project is a long-dormant internal bundle ("Api stuff for most Happyr projects") that has only recently received a patch tag (0.5.1), it has essentially no downstream users, and its API surface is designed around one company's applications rather than being framework-agnostic. The 0.5.1 patch is a one-line-ish behavior change to error mapping, not a signal of renewed maintenance. Treat it as a small, readable codebase you may need to fork, not as a dependency you can lean on for years.
composer.json first and check the symfony/* and php constraints against your project. Then read src/ top to bottom — the bundle is small enough to finish in one sitting. The …Bundle class plus Resources/config/services.{yml,xml} are the true API surface; everything else is implementation detail.composer require happyr/api-bundle, then add the bundle class to config/bundles.php (Symfony 4+) or AppKernel::registerBundles(). There is no Flex recipe, so expect no auto-generated config file and no default service wiring beyond what the bundle's own service definition provides.AccessDeniedHttpException → forbidden (403) branch, so security-driven denials are now part of that contract.Base-controller inheritance. The typical day-to-day pattern is a thin ApiController in your app that extends the bundle's base class, adds your auth/serialization helpers, and is extended by concrete controllers. Do your own subclass rather than extending the vendor class directly in every controller — it gives you one place to add request-ID logging, CORS headers, or a version header, and one place to swap out the parent if you later drop the bundle.
Exception-to-response mapping. Wire your domain exceptions to the bundle's error handling so controllers stay free of response formatting. Prefer this over per-action try/catch; the payoff is a single, testable place where HTTP status codes and payload keys are decided. With 0.5.1, AccessDeniedHttpException is mapped to a forbidden response out of the box, which means denials thrown from voters, denyAccessUnlessGranted(), or #[IsGranted] inside a controller will land in the bundle's envelope rather than falling through to a generic 500 or the framework default. That is the good news; the caveat is that Symfony's security component also has its own entry point / access-denied handler chain, which fires before the kernel exception listener in some configurations. Decide explicitly which one owns the 403 payload rather than letting both exist. If the bundle's mapping is still too coarse, decorate its listener rather than modifying it (see Gotchas).
Serialization boundary. Keep the bundle responsible only for the HTTP envelope (status, error body, pagination metadata) and let Symfony's serializer or your own normalizers own the payload. Mixing the two is where these small internal bundles usually become hard to reason about.
Pagination for list endpoints. If the bundle ships pagination helpers, use them only if your data source is Doctrine ORM. For anything else (Elasticsearch, external APIs, cursors) you will fight the abstraction — return your own metadata object instead and keep the response shape identical.
Coexistence with API Platform / FOSRestBundle. Do not stack this bundle with API Platform. They both want to own exception handling and response shaping, and the conflict resolution will cost you more than either library saves — the new built-in 403 branch makes the overlap with API Platform's ExceptionListener and security integration worse, not better. If you are already on API Platform, you probably only want one or two classes from this package — copy them into your app under MIT terms rather than installing the bundle.
Testing. Write one functional test per error class asserting status code and JSON keys, and one per list endpoint asserting pagination keys. Add an explicit test asserting that an access-denied path yields 403 with your expected body — that is now behavior you depend on, and it is behavior a patch release just changed. Because the bundle is otherwise unmaintained, these tests are your regression net for a future Symfony upgrade where the listener hooks may silently stop firing.
AccessDeniedHttpException → 403 forbidden response means that if you were previously relying on access-denied cases producing a different status code, a different payload key, or your own security entry point's body, upgrading from 0.5.0 will silently change your API contract. Semver discipline here is loose — assume any future tag can alter payload shape. Diff the listener before every bump and re-run your 403 test.AccessDeniedHandlerInterface / firewall entry point and the bundle's listener can both try to answer the same denial. The entry point handles denials raised in the firewall/voter layer before the controller runs; the listener handles ones that escape into the kernel. In practice you can end up with the entry point's response for access_control denials and the bundle's envelope for in-controller denials — two shapes for the same 403. Pick one and configure the other away.ContainerInterface::get() usage, EventDispatcherInterface signature changes, and any services.yml using deprecated tags or %parameter% forms that no longer resolve. These fail as container compile errors, not runtime exceptions, so they surface at cache warmup.config/packages/happyr_api.yaml. Any configuration must be written by hand from the bundle's own Configuration class — read it to learn the real option names rather than guessing from the README, which may be stale relative to the code.bin/console debug:container --tag=<tag> to see what the bundle registered, and bin/console debug:event-dispatcher to confirm the exception listener is actually attached. A silent listener is the most common "why is my error format wrong" cause. For 403-specific problems, also check whether a security entry point is answering first — the event dispatcher will show the listener registered even when it never gets called.decorates:; (4) fork. Avoid subclassing vendor services and modifying them — you lose the upgrade path entirely, even if that path is currently theoretical. If you need a 403 body different from what 0.5.1 produces, decorate the listener or override the response factory rather than editing vendor code.How can I help you explore Laravel packages today?