Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Inertia Bundle Laravel Package

creativspeed/inertia-bundle

Modern Inertia.js integration for Symfony 8 and PHP 8.2+, letting you build Vue or React single-page apps without an API. Includes Inertia v2 protocol support, server-side rendering, Vite integration, Twig helpers, testing assertions, and automatic flash data sharing.

View on GitHub
Deep Wiki
Context7

Getting Started

Mental model: what maps from Laravel

You already know the protocol; almost everything here is a rename plus DI instead of facades.

Laravel (inertia-laravel) This bundle
Inertia::render('Users/Index', $props) $inertia->render('Users/Index', $props) (inject InertiaInterface)
Inertia::share('auth', ...) in HandleInertiaRequests middleware $inertia->share(...) from a kernel event listener
$request->user() shaped in middleware InertiaAuthUserInterface on your User entity, shared automatically
with('success', ...) → auto-shared flash Flash bag shared automatically
$errors shared by middleware Not automatic — use redirectWithErrors()
to_route('dashboard') / back() $inertia->redirect('/dashboard') / $inertia->back()
Inertia::lazy() / optional() / defer() / merge() fn () => ... / $inertia->defer() / $inertia->merge() / $inertia->deepMerge()
assertInertia(fn (Assert $page) => ...) InertiaTestTrait::assertInertia()
@inertia, @inertiaHead, @vite Blade directives inertia() / inertiaHead() (or inertia_body/inertia_head) Twig functions + vite_entry_*_tags
php artisan inertia:middleware Nothing to scaffold — bundle uses AbstractBundle with zero config

Minimal setup

  1. Install the bundle and the Vite bundle (the root template calls its Twig functions — the README omits this):

    composer require creativspeed/inertia-bundle:^2.0 pentatrion/vite-bundle
    

    Verify the bundle registered: bin/console debug:container | grep -i inertia.

  2. Configure config/packages/inertia.yaml:

    inertia:
        root_view: 'inertia/app'   # templates/inertia/app.html.twig
        ssr:
            enabled: false
            url: 'http://127.0.0.1:13714'
        # version: omitted — v2 derives it from the Vite manifest
    
  3. Check the real Twig function names before writing the layout — the README says inertia()/inertiaHead(), the release notes say inertia_body()/inertia_head():

    bin/console debug:twig | grep -i inertia
    
  4. Root template templates/inertia/app.html.twig — the escaping on data-page is non-negotiable:

    <!DOCTYPE html>
    <html>
    <head>
        {{ inertiaHead(page) }}
        {{ vite_entry_link_tags('app') }}
    </head>
    <body>
        <div id="app" data-page="{{ page|json_encode|e('html_attr') }}">{{ inertiaBody(page) }}</div>
        {{ vite_entry_script_tags('app') }}
    </body>
    </html>
    
  5. First controller — same shape as the Laravel version, but with constructor/method injection:

    #[Route('/', name: 'home')]
    public function index(InertiaInterface $inertia): Response
    {
        return $inertia->render('Home', ['title' => 'Hello']);
    }
    

First use case

Port one of your existing Laravel Inertia pages — a dashboard — and notice what you don't write: no HandleInertiaRequests::share() for auth.user, no flash plumbing, no version string.

return $inertia->render('Dashboard/Index', [
    'stats' => $statsService->summary(),
    'recentOrders' => fn () => $orderRepo->recentFor($this->getUser()),
]);

The fn () => prop is a lazy prop: skipped on the first load, evaluated only on ?only=recentOrders. That is the same contract as Laravel's Inertia::lazy().


Implementation Patterns

Global sharing: replace your HandleInertiaRequests

This is the single biggest architectural shift. Laravel shares once per request via middleware; in Symfony you register a listener:

final class InertiaShareListener
{
    public function __construct(private InertiaInterface $inertia) {}

    public function onKernelRequest(RequestEvent $event): void
    {
        if (!$event->isMainRequest()) {
            return;
        }

        $this->inertia->share('locale', $event->getRequest()->getLocale());
        $this->inertia->share('features', ['billing' => true]);
    }
}

Register it in services.yaml with kernel.event_listener. Keep manual shares to genuinely cross-cutting data — auth and flash are already handled by the bundle.

Auth sharing via the entity, not an array

Implement InertiaAuthUserInterface on your User so the bundle knows what to send. This replaces the 'auth' => ['user' => $request->user()->only(...)] block you'd maintain by hand — and it's the place to enforce “never send password_hash”:

class User implements InertiaAuthUserInterface
{
    public function toInertiaArray(): array
    {
        return ['id' => $this->id, 'name' => $this->name, 'roles' => $this->roles];
    }
}

Prop semantics cheat sheet

return $inertia->render('Feed', [
    'title'      => 'Feed',                                  // always, every request
    'tags'       => fn () => $this->tags(),                  // lazy: partial reload only
    'stats'      => $inertia->defer(fn () => $this->stats(),  'analytics'),
    'suggestions'=> $inertia->defer(fn () => $this->suggest(), 'social'),
    'posts'      => $inertia->merge($this->pageOfPosts()),   // append on partial reload
    'filters'    => $inertia->deepMerge($this->filters()),   // recursive merge
]);

Rules of thumb:

  • Anything expensive and non-critical → defer() with a group name; two groups render in parallel on the client.
  • Infinite scroll / pagination → merge() or the list resets to page 1 on the next partial reload. This is the bug you will hit if you skip it.
  • deepMerge() for nested filter objects, never for lists.

Navigation helpers

return $inertia->redirect('/dashboard');                       // 303 under the hood
return $inertia->back();
return $inertia->location('https://billing.example.com/portal'); // 409 + external
return $inertia->redirectWithErrors($request->getUri(), [
    'email' => 'That address is already taken.',
]);

redirectWithErrors() is your substitute for Laravel's withErrors() — it writes into the flash bag in the shape useForm expects. You must call it yourself: Symfony's validator puts errors on the Form object, not in the session, so nothing arrives in $page.props.errors for free.

Per-section root layouts

return $inertia->render('Admin/Dashboard', $props, ['root_view' => 'admin']);

Same idea as Laravel's separate Blade layout per section, just per-request instead of per-route-group.

SSR

Set ssr.enabled: true and run your Vue/React SSR server on the configured port. The Twig inertiaHead/inertiaBody pair is the hand-off: head content in <head>, rendered body inside #app, with data-page as the hydration payload. If the SSR process is down, confirm whether the bundle fails open (client-side render) or hard-errors — test by killing the node process and loading a page.

Testing with WebTestCase

use CreativSpeed\InertiaBundle\Testing\InertiaTestTrait;

class DashboardTest extends WebTestCase
{
    use InertiaTestTrait;

    public function test_dashboard_props(): void
    {
        $client = static::createClient();
        $client->loginUser($user = $this->createUser());

        $client->request('GET', '/dashboard');

        $this->assertInertia($client->getResponse(), function ($page) {
            $page->component('Dashboard/Index')
                 ->has('stats.visits')
                 ->where('title', 'Dashboard');
        });
    }
}

Note what you can't do that you could in Laravel: no $this->actingAs() helper chain, and partial-reload assertions (->reloadOnly(), ->missing()) may not exist. For partial reload coverage, hit the endpoint with the real headers instead — see the curl trick below.


Gotchas and Tips

Maturity is the headline risk. 0 stars, 0 dependents, one release, one maintainer, and it targets Symfony 8 (with v2 release notes dated 2026). Pin an exact version, commit composer.lock, and budget time to read vendor/creativspeed/inertia-bundle/src — the docs are thin enough that the source is the real spec. Have a fallback plan: the protocol is simple enough to hand-roll a response factory if you must.

Docs drift in three places — all worth verifying against source on day one:

  1. README says “Inertia.js v2 protocol,” the v2 release notes say “v3 protocol.” Prop features are the same either way, but the version claim affects which client adapter you install.
  2. Twig helper names: inertia()/inertiaHead() vs inertia_body()/inertia_head().
  3. render()'s third argument: README shows ['root_view' => 'admin'] as options; the assessment shows a two-arg form.

No Ziggy equivalent. Laravel hands you route() in JS. Here you either pass URLs as props, install fos-routing-js, or hardcode paths in components. Decide before you write 30 components with hardcoded URLs.

CSRF will bite you. Inertia's default axios config reads XSRF-TOKEN from a cookie and sends X-XSRF-TOKEN — Symfony does not set that cookie. Either emit the token into the Twig shell as a <meta name="csrf-token"> and configure axios, or use Symfony's stateless CSRF. Symptom: every form.post() returns 419-ish and onError never fires.

Don't pass Doctrine entities straight into props. Laravel developers are used to JsonResource normalization. Here props are json_encoded, so a managed entity gives you circular-reference errors from lazy proxies or a leaked passwordHash field. Always map to arrays/DTOs, or implement JsonSerializable explicitly.

Validation errors are manual. See above — redirectWithErrors() or nothing. Also check whether flash data arrives under the key your Vue components expect; inspect the payload rather than guessing.

The Vite Twig functions come from pentatrion/vite-bundle. Missing it means a Unknown "vite_entry_script_tags" function error that looks like an Inertia bug.

Version mismatch → 409, not a page load. If you see a full-page reload instead of an XHR after a deploy, the manifest-derived version changed and the client's X-Inertia-Version is stale. That's expected (the bundle converts it), but it will surprise you during hot-reload — clear the Vite dev cache and rebuild.

Debug by inspecting the protocol directly. Send the headers Inertia sends and read the JSON:

curl -s -H 'X-Inertia: true' -H 'X-Inertia-Version: <manifest-hash>' \
     -H 'X-Requested-With: XMLHttpRequest' http://localhost:8000/dashboard | jq .
curl -s -H 'X-Inertia: true' -H 'X-Inertia-Partial-Data: recentOrders' \
     -H 'X-Inertia-Partial-Component: Dashboard/Index' http://localhost:8000/dashboard | jq .

The second call is how you verify lazy/deferred/merge props actually behave — far faster than clicking around, and it doubles as your partial-reload test.

Long-running workers. Symfony under FrankenPHP/RoadRunner keeps services alive between requests. If share() accumulates on a singleton, data leaks across users — catastrophic for an auth prop. Test with a two-request sequence as different users; if it leaks, move sharing into a request-scoped listener that resets, or decorate the service with ResetInterface.

Escaping. data-page="{{ page|json_encode|e('html_attr') }}" — never |raw. A </script> in a user-supplied prop is stored XSS otherwise.

Extension points, in order of usefulness: decorate InertiaInterface (cleanest way to add cross-cutting props or tracing); implement InertiaAuthUserInterface for auth shaping; a kernel.controller listener for per-controller sharing; per-request root_view for multi-layout apps. There's no middleware stack to hook, so decoration plus listeners is the whole toolbox.

Budget for missing Laravel ergonomics. No inertia:start-ssr, no dev-tooling, no AssertableInertia parity, no documented upgrade path. Treat v1 → v2 as a rename-and-verify exercise (the v2 notes flag two behaviour changes: automatic 302→303 conversion and asset-version conflict handling) and re-read the source diff rather than trusting the migration prose.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log