xi/ajax-bundle
xi/ajax-bundle offers a lightweight AJAX layer for PHP applications, simplifying asynchronous request handling and JSON responses. It cuts boilerplate for AJAX endpoints and integrates cleanly with existing controllers, making it a simple, dependency-light toolkit for small projects.
Reality check first: xi/ajax-bundle is a Symfony 2 bundle, not a Laravel package. It predates modern PSR-4 conventions and targets PHP 5.3/5.4-era Symfony. If you're in a Laravel codebase, you will not install this — Laravel's $request->expectsJson() / $request->ajax() plus response()->json() already cover the same ground. The useful way to read this package as a Laravel developer is as a reference for how a framework-agnostic "respond with JSON when the client asked for it" helper can be structured, and as a warning about tight coupling to one framework's kernel.
If you find yourself in a legacy Symfony 2 app that already depends on it:
composer require xi/ajax-bundle (expect old symfony/symfony constraints to fight with anything modern; you'll likely need --ignore-platform-reqs or a pinned composer).app/AppKernel.php inside registerBundles().DependencyInjection/ config and its controller helper/service — that service is the entry point. Then read its exception listener, which is where the "return JSON errors instead of HTML pages for AJAX requests" behaviour lives.The Laravel translation of the first use case:
public function index(Request $request)
{
$data = ['items' => Item::all()];
if ($request->expectsJson()) {
return response()->json(['ok' => true, 'data' => $data]);
}
return view('items.index', $data);
}
That is the entire value proposition of xi/ajax-bundle, minus the bundle ceremony.
1. Content negotiation instead of a boolean flag. The bundle's core idea is: inspect the request (typically X-Requested-With: XMLHttpRequest and/or Accept: application/json) and pick a representation. In Laravel, prefer expectsJson() over ajax() — ajax() only checks X-Requested-With (jQuery-era), while expectsJson() also honours Accept. This matters for fetch/axios clients that don't send the XHR header.
2. Consistent response envelope. If you're porting the bundle's pattern, define one envelope shape and stick to it. The bundle wraps payloads so the front end can branch on a single field:
// app/Http/Responses/ApiResponse.php
class ApiResponse
{
public static function ok(mixed $data = null, int $status = 200): JsonResponse
{
return response()->json(['ok' => true, 'data' => $data, 'error' => null], $status);
}
public static function fail(string $message, int $status = 422, array $errors = []): JsonResponse
{
return response()->json(['ok' => false, 'data' => null, 'error' => ['message' => $message, 'fields' => $errors]], $status);
}
}
3. Exception-to-JSON conversion. The bundle's listener is the analogue of Laravel's App\Exceptions\Handler::render() / shouldReturnJson(). Laravel already returns JSON automatically when expectsJson() is true, so the pattern to replicate is shaping it:
// bootstrap/app.php (Laravel 11) or Handler::register()
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (ValidationException $e, Request $request) {
if ($request->expectsJson()) {
return ApiResponse::fail('Validation failed', 422, $e->errors());
}
});
})
4. Form/controller split. Rather than a base controller trait (the bundle's approach), keep responses in a dedicated layer — a RespondsWithJson trait, a response factory, or API Resources (JsonResource::collection(...)) if you're returning models. This avoids the bundle's main structural weakness: forcing every controller to inherit from a specific base class.
5. Blade + JSON from the same endpoint. Useful for progressively enhanced pages: let the same route serve HTML to browsers and JSON to scripts, branching only on expectsJson(). Do not put this branching in every action — centralise it in a middleware or a small respond() helper on a base controller.
X-Requested-With is not reliable. The bundle (and $request->ajax()) trusts a header that modern clients often omit. Always prefer Accept: application/json content negotiation (expectsJson()), and be explicit in your API clients.VerifyCsrfToken's except for stateless API routes or send X-CSRF-TOKEN / _token. The bundle doesn't solve this; expect to wire it yourself.Accept header wasn't set. Debug by logging $request->header('Accept') and $request->expectsJson() at the top of the action.APP_DEBUG=true, Laravel's whoops-style output is HTML. If you're consuming your own API from JS, either accept Accept: application/json on every call or assert expectsJson() in your exception renderer.$exceptions->render(...) callbacks and Responsable classes give you the same hook with better ergonomics.{ok, data, error} (or Laravel's native {data, errors} from Resources) beats per-endpoint ad-hoc shapes when you're debugging in the browser network tab and writing front-end handlers.expectsJson(); bundle JSON response factory → response()->json() or Resources; bundle exception listener → Exceptions::render in bootstrap/app.php. No bundle required.How can I help you explore Laravel packages today?