jaxon-php/jaxon-laravel
Laravel integration for Jaxon, the PHP library for AJAX-driven apps. Expose PHP classes and methods to JavaScript, register Jaxon services, and manage config, routes, and views using Laravel conventions. Includes a service provider, facade, config publishing, and Blade helpers for adding AJAX functi...
Install and publish
composer require jaxon-php/jaxon-laravel
php artisan vendor:publish --provider="Jaxon\Laravel\JaxonServiceProvider"
# if the provider tag differs in your version:
php artisan vendor:publish --tag=jaxon-config
This drops config/jaxon.php (request URI, JS lib URI, asset/export options, pre-registered classes) and, in most versions, auto-registers the AJAX endpoint route plus an asset route. Confirm both before writing any code:
php artisan route:list | grep -i jaxon
# typically: POST /jaxon and GET /jaxon/asset/{name}
Where to look first, in this order
config/jaxon.php — the core.request.uri value must match what the JS actually posts to. This single key causes most first-day failures.vendor/jaxon-php/jaxon-laravel/src/ — the service provider, the controller that handles calls, and the Blade directives. Small package; read it in ten minutes.vendor/jaxon-php/jaxon-core/ — Jaxon 5 renamed several helpers across majors. When a snippet from a blog post doesn't resolve, the core source is the truth, not the docs site.First use case: one exported class, one call
Annotate a plain PHP class. Methods become callable from JS; the return value arrives as the JS callback argument.
namespace App\Ajax;
use Jaxon\Attributes\Attribute\Jaxon; // verify the exact namespace for your major
use Jaxon\Attributes\Attribute\Exclude;
#[Jaxon]
class Hello
{
public function sayHello(string $name): array
{
return ['message' => "Hello {$name}", 'at' => now()->toIso8601String()];
}
#[Exclude]
public function internalHelper(): void {}
}
Register it (config key = the JS-visible name), or register at runtime in a service provider:
// config/jaxon.php
'classes' => [
'hello' => \App\Ajax\Hello::class,
],
// or, at runtime
use Jaxon\Laravel\Facades\Jaxon;
Jaxon::register(Jaxon::CALLABLE_CLASS, \App\Ajax\Hello::class, ['alias' => 'hello']);
Then render the scripts in your layout and call it:
<head>
@jaxonCss
</head>
<body>
@jaxonJs
@jaxonScript
<button onclick="jaxon.hello.sayHello('Ada').then(r => console.log(r.message))">
Say hello
</button>
</body>
If the button 404s, the URI is wrong. If it 419s, it's CSRF (see Gotchas). If jaxon.hello is undefined, the class isn't registered or the export cache is stale.
Config-first registration, runtime only for conditionals. Keep the class map in config/jaxon.php so config:cache works and the export set is deterministic. Reach for Jaxon::register() only when the set is genuinely request-dependent (per-tenant classes, feature-flagged modules) and register it from a service provider's boot(), not from a controller.
Return a Response for DOM work, return data for logic. Plain return values round-trip as JSON to the JS callback. When the server should decide what the page looks like, build a response:
public function addToCart(int $id, int $qty): \Jaxon\Response\Response
{
$count = Cart::add($id, $qty);
$response = jaxon()->newResponse();
$response->assign('cart-count', 'innerHTML', (string) $count);
$response->script('window.dispatchEvent(new Event("cart:updated"))');
return $response;
}
This keeps markup in Blade and mutations in PHP, which is the whole reason to use Jaxon over hand-written fetch() handlers.
Server-initiated calls. From a normal (non-AJAX) controller action, queue JS calls that run on page load:
jaxon()->call('hello.sayHello', 'World'); // fluent variants exist as ->with(...)
return view('dashboard');
Useful after a redirect-back-with-flash, or to push a toast after a form post that didn't go through Jaxon at all.
Middleware and authorization. The auto-registered route is convenient but usually lands in the web group with whatever that implies for your app. Two robust options:
core.request.uri at your path.$this->authorize(...) or a Gate::check() at the top. There is no implicit "only logged-in users" guarantee.A per-call guard in a base class is the cheapest pattern:
abstract class AjaxController
{
protected function guard(string $ability, $arg = null): void
{
abort_unless(Gate::allows($ability, $arg), 403);
}
}
Uploads, pagination, and long lists. Jaxon ships helpers for paginated responses and file uploads; use them rather than hand-rolling offset math in JS. Pagination state belongs in the request (page number argument), because each AJAX call is a fresh HTTP request — anything kept only in a PHP property is unreliable (see Gotchas).
Testing. Feature-test the endpoint directly rather than driving a browser:
$response = $this->post('/jaxon', [
'jxncls' => 'hello',
'jxnmthd' => 'sayHello',
'jxargs' => ['World'],
]);
$response->assertOk();
$this->assertStringContainsString('Hello World', $response->getContent());
Parameter names (jxncls/jxnmthd/jxargs) have shifted between majors — capture one real request from the browser network tab once and codify it. Also unit-test the underlying service classes without Jaxon at all; the export layer is thin and shouldn't be where your business logic gets tested.
Blade integration. The directives (@jaxonCss, @jaxonJs, @jaxonScript) are the supported path; the underlying jaxon()->css() / ->js() / ->script() calls exist if you need to place them conditionally or inside a component. Keep all three in the layout — a missing @jaxonScript produces a page where jaxon.* exists but is inert, which is maddening to debug.
CSRF is the #1 day-one failure. Jaxon posts to its own endpoint, and if that route sits in the web group the POST gets a 419. Either add the URI to VerifyCsrfToken::$except, or send the token with the request:
// in a service provider boot(), or via config
jaxon()->setOption('core.request.headers', ['X-CSRF-TOKEN' => csrf_token()]);
Prefer the header approach — it keeps CSRF protection. If you use it, the token must be rendered server-side on every page that makes Jaxon calls.
URI drift. core.request.uri appears in the generated JS, in the route registration, and in any proxy/CDN config. Change one, and you get a 404 that looks like a JS bug. Always cross-check with route:list after touching it.
Stale exports after config changes. php artisan config:cache bakes config/jaxon.php; php artisan optimize:clear after edits. In production, minified/exported JS is generated from the registered class list — if you add a class and the browser still says undefined, clear caches and hard-reload before debugging anything else.
Anything printed breaks the response. The AJAX endpoint returns JSON. A stray dump(), dd(), a BOM in a PHP file, a var_dump left in a trait, or a debugbar that echoes into the body will produce a parse error in the browser with a 200 status. When calls "silently fail," curl the endpoint and look at the raw body first.
Instance state is not per-request-safe. Depending on version and session config, Jaxon can keep registered class instances around between calls rather than reconstructing them. Never store request-scoped data (the current user, the current model, a validated payload) on properties and assume it's fresh — read from request(), auth(), or method arguments inside each exported method. This is also a cross-user data-leak risk if you get it wrong.
Every exported method is a public endpoint. Attributes and #[Exclude] control exposure, not authorization. Audit the export list before shipping: jaxon()->getClasses() or the generated JS config is the ground truth for what an attacker can invoke. Methods marked public on an exported class are reachable — protected/private are not, which is a useful first line of defense.
Asset serving. If assets are served through the package's asset route, they inherit your middleware — a auth middleware on that route means logged-out users get broken JS. Exclude the asset route from auth. Alternatively serve from CDN via the js.lib.uri option, but then you own CSP (script-src) and version pinning.
Errors surface as JS rejections, not Laravel error pages. Exceptions thrown in exported methods come back as a failed Jaxon call. Register a handler that logs with context and returns a user-safe message, otherwise production errors become silent console.error noise nobody sees. Keep APP_DEBUG behavior in mind: debug responses can leak stack traces into the JSON payload.
Ecosystem risk. Ten stars and a small user base means fewer Stack Overflow answers and slower upstream fixes. Pin the exact version in composer.json, read the changelog before minor bumps (Jaxon's majors have renamed helpers and attributes), and keep your exported classes thin wrappers over ordinary service classes you could call from a plain controller if you ever need to migrate off. The BSD-3 license is permissive enough that vendoring or forking the integration layer is a realistic escape hatch.
When not to use it. For a handful of endpoints, a JSON controller plus fetch() is less machinery and easier for the next developer to read. Jaxon earns its keep when you have many small, DOM-mutating interactions and want them authored in PHP — think admin panels, dashboards, and form-heavy internal tools.
How can I help you explore Laravel packages today?