jaxon-php/jaxon-core
Jaxon core lets PHP classes and functions be called directly from JavaScript in Ajax applications. It exports server-side code to the browser, handles incoming Ajax requests, generates responses, and manages the client-side Jaxon object with configuration and plugins.
Understand what you're installing first. jaxon-php/jaxon-core is the engine, not the Laravel integration. It gives you request parsing, callable-class dispatch, the response/command system, and the plugin API — but no service provider, no config publishing, no route, and no Blade directives. In a Laravel app you almost always want the wrapper:
composer require jaxon-php/jaxon-laravel # pulls jaxon-core + jaxon-js
php artisan vendor:publish --tag=config # -> config/jaxon.php
If you're auditing or maintaining a bare-core install, the day-one wiring is: read config (jaxon()->config()->read(...)), register callables, then hand the incoming request to jaxon()->processRequest() from a front controller. That's the whole contract — everything else is optional.
Where to look first
| Place | Why |
|---|---|
config/jaxon.php |
The single source of truth: export list, request URI, JS lib/app URIs, response format, debug flags |
app/Ajax/ (you create it) |
Convention for exported callable classes; keeps remotely-callable code in one auditable place |
vendor/jaxon-php/jaxon-core/src/Plugin/ |
The extension surface — most "how does this work" questions end here |
| The network tab | Every Jaxon response is a JSON array of commands; reading it explains 90% of bugs |
| jaxon-php.org | Docs are current for v5; blog posts and StackOverflow answers are mostly Xajax/Jaxon 3 era |
First use case — replace one jQuery $.ajax round trip.
namespace App\Ajax;
use Jaxon\CallableClass;
class Hello extends CallableClass
{
public function sayHello(string $name): void
{
$this->response->assign('message', 'innerHTML', "Hello {$name}!");
}
}
Export it (config export.classes, or jaxon()->register(Jaxon::CALLABLE_CLASS, Hello::class) in a service provider's boot()), then in Blade:
@jaxonCss
@jaxonJs
<button onclick="jaxon_app_ajax_hello.sayHello('World').call()">Say hello</button>
<div id="message"></div>
The JS object name is derived: prefix jaxon_ + lowercased fully-qualified class name with \ → _. App\Ajax\Hello → jaxon_app_ajax_hello. Click the button, watch a POST to /jaxon, and you've seen the whole loop.
Prefer response commands over return values for UI work. Returning a value sends it to the JS callback; writing to $this->response mutates the page. In day-to-day Laravel work the response style is usually what you want:
public function save(int $id, string $title): void
{
$post = Post::findOrFail($id);
$post->update(['title' => $title]);
$this->response->assign("title-{$id}", 'innerHTML', e($post->title));
$this->response->script("window.toast('Saved')");
$this->response->remove("spinner-{$id}");
}
Send parameters as request objects, not as interpolated strings. Hard-coding onclick="...('World')" breaks the moment the value contains a quote. Use the parameter helpers so Jaxon serializes correctly:
<button onclick="jaxon_app_ajax_hello.sayHello(jaxon.$('name-input')).call()">Go</button>
<form onsubmit="jaxon_app_ajax_hello.save(jaxon.form('post-form')).call(); return false;">
jaxon.$('id') reads an input value; jaxon.form('id') serializes a whole form (including files). Both produce lazy request objects that are only resolved at send time, so they always read current DOM state.
Constructor injection works — lean on it. Under the Laravel wrapper, callable classes are resolved through the container, so __construct(private PostRepository $posts) is fine and is the cleanest way to keep Ajax classes thin.
Chain/join requests when the UI needs sequencing. .call() takes a callback for the return value; use the join/queue helpers when one action must follow another rather than firing three independent requests and racing them.
Authorize every method explicitly. There is no implicit auth boundary: every public method on an exported class is a public HTTP endpoint at your Jaxon URI. Either guard the route in middleware, or put $this->authorize(...)-style checks at the top of each method. Treat exported classes exactly like controllers.
Keep exports static and boring. Config-driven export lists (export.classes) beat scattered register() calls — one grep tells you the entire remote attack surface. Register dynamically only when the set genuinely depends on runtime state.
Testing. Callable classes are plain classes; instantiate them in PHPUnit and assert on the accumulated response commands rather than trying to bootstrap the HTTP layer. Anything that needs the full request cycle belongs in a feature test hitting the Jaxon route.
Version fragmentation is the biggest trap. v5 config keys, the jaxon() helper, and the JS API differ from v4, which differs heavily from v3 — and most online examples are v3 or Xajax. If a snippet references $jaxon->registerFunction() or xjax_, it will not work. Pin the major version and read the vendor source when in doubt.
CSRF and middleware. The Jaxon endpoint is a POST route inside the web group. If you exclude it from CSRF for convenience, you've opened the whole export surface; keep the token flowing instead. If you see 419s, that's the cause, not Jaxon.
Generated JS lives on disk. The app JS is written to a configured directory and served from a configured URI (js.app.dir / js.app.uri in config). If you add an exported class and the browser still 404s or calls a stale object, that's a missing/undeployed artifact or a permissions problem — not a caching mystery in your app code.
Response format mismatch. core.response.send defaults to JSON. Some plugins (notably jQuery-dependent ones) expect XML; mixing them produces silent no-ops where the request succeeds but nothing happens. Check the response body in the network tab before debugging your PHP.
Debug output is a config flag, not a log channel. Turn on core.debug.on to surface exception messages and trace data in the response. Without it, a thrown exception in a callable typically yields an opaque failure on the client — the single most common source of "my Ajax method does nothing."
Return values and commands coexist. A method that both returns a value and mutates $this->response does both. This is a frequent source of duplicated UI updates; pick one convention per class.
Types arrive loose. Parameters come off the wire and PHP 8 type hints will either coerce or throw depending on strict_types. Cast explicitly ((int) $id) at the boundary rather than trusting the type hint, and validate before touching the database.
Extension points, in order of usefulness: custom response commands (subclass/extend the response to add domain-specific DOM operations so your callables stay declarative), plugins (PluginInterface) for cross-cutting concerns like logging or transaction wrapping, and the dialog/view packages (jaxon-dialogs, view renderers) rather than hand-rolling HTML in assign() calls.
Ecosystem reality check. 74 stars, essentially single-maintainer, with framework bridges maintained separately. It's stable and genuinely good at what it does — progressive-enhancement Ajax without an SPA — but it is a minority stack. Budget for reading the source, keep the exported surface small, and don't expect modern tooling (no TypeScript definitions for the client, no Vite integration). For a greenfield Laravel app, Livewire covers the same ground with far more community support; Jaxon earns its place in legacy migrations, server-rendered apps with heavy existing JS, or codebases already using it.
How can I help you explore Laravel packages today?