tcloud.ax/symfony-jsend-bundle
Symfony JSend Bundle provides JSend-compliant JSON responses for Symfony applications, helping standardize API success, fail, and error payloads. Integrate consistent response envelopes, reduce custom serialization code, and keep controllers clean.
This is a Symfony bundle, not a Laravel package — there is no ServiceProvider, no auto-discovery, and no config/app.php entry. If you're coming from Laravel, treat it as a reference implementation of the JSend spec that you either port or consume inside a Symfony service. For day-to-day use in a Symfony app:
"repositories": [{ "type": "vcs", "url": "https://gitlab.com/tcloud.ax/symfony-jsend-bundle" }],
"require": { "tcloud.ax/symfony-jsend-bundle": "dev-main" }
config/bundles.php:
Tcloud\SymfonyJsendBundle\SymfonyJsendBundle::class => ['all' => true],
src/ for the response/factory class and any EventListener or EventSubscriber (typically a kernel.exception listener). The README, if present, will show the canonical Jsend::success() style helper. Read the exception listener first — that's where the "magic" lives and where most surprises come from.return $this->json([
'status' => 'success',
'data' => ['user' => $user->toArray()],
]);
Then wire the exception listener so thrown exceptions become {"status":"error","message":"...","code":500} instead of Symfony's HTML error page.1. Three-bucket responses. JSend has exactly three shapes — enforce them with helpers rather than free-form $this->json():
success — 2xx, data holds the payload (object/array; null allowed).fail — 4xx, data holds structured details (field → message map for validation).error — 5xx, message is human-readable, code is a machine code, data is optional.// success
['status' => 'success', 'data' => $dto]
// fail (validation)
['status' => 'fail', 'data' => ['email' => 'Invalid email']]
// error
['status' => 'error', 'message' => 'Upstream timeout', 'code' => 504]
2. Global exception listener. The highest-leverage integration is mapping domain exceptions to JSend shapes in one place:
class JsendExceptionListener {
public function onKernelException(ExceptionEvent $event): void {
$e = $event->getThrowable();
[$status, $payload] = match (true) {
$e instanceof ValidationException => [422, ['status'=>'fail','data'=>$e->errors()]],
$e instanceof NotFoundHttpException => [404, ['status'=>'fail','data'=>null]],
default => [500, ['status'=>'error','message'=>'Internal error','code'=>500]],
};
$event->setResponse(new JsonResponse($payload, $status));
}
}
Register with a high priority so it wins over Symfony's default ErrorListener.
3. Validation via fail. Map Symfony Validator errors into the data object as field => message[]. Clients can then render forms directly from response.data without parsing a generic message.
4. Serializer wrapping. Normalize your entity/DTO with Symfony Serializer (respecting @Groups), then wrap the normalized array as data. Keep the JSend envelope outside the serializer to avoid polluting your DTOs with status fields.
5. Status code discipline. Set the HTTP status to match the JSend status: success → 200/201, fail → 400/401/403/404/422, error → 500+. Clients (and API gateways) key off both, but they must agree.
6. Testing assertions.
$client->request('GET', '/api/users/1');
$this->assertResponseIsSuccessful();
$this->assertJsonPath('status', 'success');
$this->assertJsonStructure(['status', 'data' => ['id', 'email']]);
7. Laravel translation. If you're actually on Laravel, don't fight the bundle — copy its three response shapes into a small JsendResponse trait or a package you control, and register a Handler::render override in app/Exceptions/Handler.php to emit the envelopes. The spec is trivial; the value is the discipline, not the code.
dev-main constraint, a GitLab auth token for CI, and no community fixes. Pin a commit hash (dev-main#abc1234) rather than a branch to survive force-pushes upstream.config/jsend.php published by default. Budget an hour to read the source before assuming APIs from the README exist."status":"fail" confusion. Some teams return 200 for everything and rely on the body; others (correctly) match status codes. Pick one convention before frontend work starts — mismatches are the #1 JSend integration bug.data type drift. JSend says data is an object/array/null, but nothing stops a controller returning a scalar. Add a response listener or a JsendFactory method with a typed signature so data is always arrayable.ErrorListener runs at -128. Register yours above it (e.g. -64) or every exception becomes a debug HTML page and the bundle never fires. Test with kernel.debug both on and off — behavior diverges.error path must never serialize $e->getTrace(), file paths, or SQL. Log the full exception server-side and emit only a correlation code + safe message.fail vs error semantics. Use fail only for client-caused problems (validation, missing auth, bad input). Using fail for a 500 hides real outages from monitoring that greps for status:error.application/json. Ensure your listener sets it explicitly — Symfony's JsonResponse does, but a manually built Response with json_encode won't.{"data": {...}} get double-wrapped by the listener. Audit routes after installation; grep for existing json()/JsonResponse returns.kernel.exception / kernel.response event subscribers — those two events cover ~90% of custom JSend behavior (adding pagination, meta, or request-id fields).How can I help you explore Laravel packages today?