phptcloud/symfony-jsend-bundle
Symfony bundle for JSend-compliant JSON responses. Standardize API output with consistent status, data, message and code fields, making client-side handling simpler. Integrates easily into Symfony controllers and services.
Install via Composer:
composer require phptcloud/symfony-jsend-bundle
If you are not on Symfony Flex, register the bundle manually in config/bundles.php:
return [
// ...
PHPTCloud\JsSendBundle\JsSendBundle::class => ['all' => true],
];
Look first at the bundle's DependencyInjection/Configuration.php and Resources/config/services.yml in the installed package (typically under vendor/phptcloud/symfony-jsend-bundle/). As of v3.0.2 the services file auto-wires JsendStandard\Interfaces\ResponseFactoryInterface, so the single most useful thing you can do on day one is confirm the alias is present and that you can type-hint the interface in a controller constructor. Note the namespace: the interface lives in JsendStandard\Interfaces, not under PHPTCloud\JsSendBundle — type-hint the interface, not a concrete bundle class, or autowiring will miss.
Because this is a small package (14 stars, single-maintainer), read the source rather than trusting blog posts — the public API is small enough to scan in a few minutes, and the interface/service wiring is now the part most worth verifying.
The first use case is returning a controller response in JSend shape. Either inject the factory:
use JsendStandard\Interfaces\ResponseFactoryInterface;
public function __construct(private ResponseFactoryInterface $jsend) {}
#[Route('/api/users/{id}', methods: ['GET'])]
public function show(User $user): Response
{
return $this->jsend->success([
'id' => $user->getId(),
'name' => $user->getName(),
]);
}
or, if you prefer the static shortcut, use the bundle's response class:
use PHPTCloud\JsSendBundle\Response\JsSendResponse;
return JsSendResponse::success([
'id' => $user->getId(),
'name' => $user->getName(),
]);
That yields {"status":"success","data":{...}} with a 200. For failures use fail([...]) (client error, data is field-level detail) and error($message, $code, $data) (server error / processing exception). If the exact helper names differ in your installed version, inspect the interface and JsSendResponse.php — the JSend spec requires the status key to be one of success, fail, error, so the factory methods will inevitably map to those three.
Inject the factory, not the static helper. Since v3.0.2 ResponseFactoryInterface is auto-wired, so constructor injection is now the low-friction default. This is worth standardising on across the codebase: it makes controllers mockable in unit tests, and it gives you one seam to decorate if you later want a request ID or timestamp on every envelope. Reserve the static JsSendResponse::success(...) calls for one-off scripts or places where DI is genuinely awkward.
Wrap every JSON endpoint. The point of JSend is consistency. Adopt a rule that no controller returns a raw JsonResponse. Return a response built through the factory so front-end code can branch on response.status once, everywhere.
Exception → JSend via kernel events. The common integration is a listener/subscriber on kernel.exception that detects ApiException-style exceptions and renders fail(...) or error(...). Inject the same ResponseFactoryInterface into the subscriber so the envelope shape is identical to the happy path. If the bundle does not ship a listener, add your own so that uncaught ValidationException becomes:
{"status":"fail","data":{"email":"Not a valid email"}}
and uncaught ServerException becomes:
{"status":"error","message":"Internal error","code":500}
Keeping this in one subscriber avoids scattered try/catch in controllers.
Serializer integration. For entity-backed payloads, normalize first and pass the array into the factory. This keeps the response object dumb and lets you test normalization separately:
$payload = $this->serializer->normalize($order, null, ['groups' => ['api:read']]);
return $this->jsend->success($payload);
Validation and errors are different statuses. JSend distinguishes fail (the client sent bad input — HTTP 4xx) from error (the server failed — HTTP 5xx). Map your domain exceptions accordingly, and set the actual HTTP status code on the response as well; JSend's status is not a replacement for the status line. Browsers, proxies, and HTTP clients still look at 400/500.
Testing. assertJson plus a structural check is enough; don't snapshot entire payloads. A useful base assertion:
$this->assertSame('success', $response->json('status'));
$this->assertArrayHasKey('data', $response->json());
If you inject the factory, you can also assert in a unit test that the controller passed the expected payload to success() without booting a kernel.
JsendStandard\Interfaces\ResponseFactoryInterface. If you type-hint a concrete class from PHPTCloud\JsSendBundle (or the interface before you upgrade), you can still hit "no such service." If you're on an older version and can't upgrade yet, define an explicit alias in config/services.yaml — but upgrading is the cleaner fix.data is required for success and fail. The spec allows data to be null for error but requires it for the other two. Passing an empty array is fine; omitting the key is not. If the factory signature takes ?array $data = null, be explicit and pass [].success($entity) will work only until the entity has a lazy relation, a circular reference, or a DateTime that should be ISO-8601. Always normalize/serialize first, or you will find yourself debugging JsonException: Recursion detected.fail(...) with a 200 because the factory defaults to 200. Check the interface signature and pass the HTTP code explicitly: fail($errors) should typically be a 400/422, error(...) a 500. Search the bundle source for setStatusCode to confirm the defaults.error responses. Only include debug detail when APP_ENV=dev. In production, message and code should be opaque. If the bundle's built-in exception listener exposes getMessage() verbatim, override it with your own subscriber.Accept header before JSend ever sees it. Either disable format_listener for JSON routes or make JSend responses bypass content negotiation by setting Content-Type: application/json directly.decorates: key makes that trivial now that there is a stable interface to decorate — the v3.0.2 wiring makes this materially easier than wrapping a static helper. Also check Resources/config/services.yml for other services marked public: true — the factory is covered, but anything else the bundle registers may still be private.dump() in the controller won't show what the listener did). If the envelope looks wrong, var_dump($response->getContent()) in a functional test is faster than clicking through requests. When injection misbehaves, bin/console debug:container JsendStandard\\Interfaces\\ResponseFactoryInterface tells you immediately whether the alias resolved.composer.json. That said, v3.0.2 is a responsive, low-risk patch that improves DI ergonomics — a good signal. The JSend spec is a two-page document; if the maintainer goes inactive, a hand-rolled response class implementing the same interface is a few dozen lines.How can I help you explore Laravel packages today?