Symfony mit Inertia.js kombinieren: SPA-Gefühl ohne eigene API
AI generated
SF
{ }
Symfony · Inertia.js · React · SPA
Symfony mit Inertia.js kombinieren
SPA-Gefühl ohne eigene API

Symfony mit Inertia.js zu kombinieren bedeutet, Controller weiterhin klassisch Twig-artig zu schreiben, nur dass am Ende statt HTML ein React-Komponentenname mit Props zurückgegeben wird, wodurch das gesamte Frontend wie eine Single-Page-Application wirkt, ohne dass irgendwo eine separate REST- oder GraphQL-API entsteht.

19 Min. Lesezeit Symfony Inertia.js · React · Props Symfony 7.x · PHP 8.4

1. Was Symfony mit Inertia.js grundlegend anders macht

Wer Symfony mit Inertia.js kombiniert, verzichtet bewusst auf den sonst üblichen Weg, eine separate JSON-API zu bauen, die ein React-Frontend über Fetch-Aufrufe konsumiert. Stattdessen bleibt der Symfony-Controller die einzige Quelle der Wahrheit für eine Seite: Er lädt Daten aus Doctrine, prüft Berechtigungen über Voters, und gibt am Ende statt eines Twig-Templates den Namen einer React-Komponente zusammen mit den benötigten Props zurück. Inertia übernimmt im Hintergrund den Austausch der Seite im Browser, ohne einen kompletten Neuladen der Seite auszulösen.

Der entscheidende konzeptionelle Unterschied bei Symfony mit Inertia.js gegenüber einer klassischen API-plus-SPA-Architektur: Es gibt keine zweite Schnittstelle, die parallel zur Server-Logik gepflegt werden muss. Routing, Autorisierung und Serialisierung passieren an genau einer Stelle im Symfony-Controller, nicht doppelt in Controller und API-Ressource. Das reduziert den Wartungsaufwand erheblich, besonders in Projekten, in denen die API ohnehin nur vom eigenen Frontend genutzt wird und keine externen Konsumenten bedient.

Für Teams, die React-Komponenten für ihre Interaktivität schätzen, aber die Komplexität einer vollständigen API-Schicht mit OpenAPI-Dokumentation und Versionierung vermeiden wollen, ist Symfony mit Inertia.js ein pragmatischer Mittelweg zwischen klassischem, serverseitig gerendertem Symfony und einer vollständig entkoppelten Single-Page-Application.

2. Installation: Backend-Adapter und Frontend-Bridge

Die Einrichtung von Symfony mit Inertia.js benötigt zwei Teile: den PHP-seitigen Adapter inertiajs/inertia-laravel existiert nur für Laravel, für Symfony wird stattdessen die Community-Bibliothek inertiajs/inertia-symfony beziehungsweise ein leichtgewichtiger eigener Service genutzt, der die HTTP-Vertragsdetails von Inertia implementiert. Auf der Frontend-Seite kommt das npm-Paket @inertiajs/react zum Einsatz, das den React-Root aufsetzt und bei Navigationen die passende Seite lädt.

Wichtig bei der Konfiguration von Symfony mit Inertia.js: Das Backend muss auf den speziellen HTTP-Header X-Inertia reagieren. Ist der Header gesetzt, antwortet der Server mit reinem JSON statt einem vollständigen HTML-Dokument, weil Inertia im Frontend bereits eine laufende Seite hat und nur die neuen Props benötigt. Bei einem regulären Seitenaufruf ohne diesen Header liefert Symfony ein initiales HTML-Dokument mit eingebettetem JSON, aus dem React beim ersten Laden hydriert.


# Backend: install the Inertia HTTP contract adapter for Symfony
composer require inertiajs/inertia-symfony

# Frontend: install the Inertia React client
bin/console importmap:require @inertiajs/react
bin/console importmap:require react react-dom

3. Der erste Inertia-Controller mit Props

Ein Symfony-Controller, der Inertia.js nutzt, unterscheidet sich strukturell kaum von einem gewöhnlichen Controller. Statt $this->render('template.html.twig', [...]) aufzurufen, wird eine Inertia-Response mit dem Namen der React-Komponente und einem Array von Props zurückgegeben. Die Komponente selbst liegt als .tsx-Datei im Frontend-Verzeichnis und erhält die Props als reguläre React-Props, typsicher über TypeScript-Interfaces.

Der Vorteil dieses Musters bei Symfony mit Inertia.js: Die Struktur der Props wird zentral im Controller definiert, dort, wo auch die Autorisierungsprüfung über denyAccessUnlessGranted stattfindet. Es gibt keine separate Serializer-Konfiguration und keine zusätzliche API-Ressourcen-Klasse, die synchron zum Controller gehalten werden müsste.


// src/Controller/OrderController.php
namespace App\Controller;

use App\Repository\OrderRepository;
use Inertia\Inertia;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

final class OrderController extends AbstractController
{
    public function __construct(
        private readonly OrderRepository $orderRepository,
    ) {
    }

    #[Route('/orders', name: 'app_orders_index', methods: ['GET'])]
    public function index(): Response
    {
        $this->denyAccessUnlessGranted('ROLE_USER');

        $orders = $this->orderRepository->findRecentForCurrentUser(limit: 25);

        // Renders the React component "Orders/Index" with these props
        return Inertia::render('Orders/Index', [
            'orders' => array_map(
                static fn ($order) => [
                    'id' => $order->getId(),
                    'reference' => $order->getReference(),
                    'total' => $order->getTotalGross(),
                    'status' => $order->getStatus()->value,
                ],
                $orders,
            ),
        ]);
    }
}

4. Client-seitiges Routing ohne eigenes Backend-Routing-Duplikat

Ein häufiges Missverständnis bei Symfony mit Inertia.js: Es entsteht kein zweites Routing-System im Frontend, das parallel zu Symfonys routes.yaml gepflegt werden müsste. Navigation zwischen Seiten läuft über die Inertia-Link-Komponente, die intern eine reguläre Symfony-URL anspricht, etwa /orders/42. Der Browser wechselt die URL sichtbar in der Adressleiste, Inertia fängt den Klick jedoch ab, verhindert den vollständigen Seiten-Reload und lädt nur die neuen Props per Fetch nach.

Das bedeutet für Symfony mit Inertia.js: Die einzige Quelle der Wahrheit für URLs bleibt Symfonys Routing-Komponente. Um im React-Code typsichere URL-Generierung zu ermöglichen, nutzen viele Projekte zusätzlich ein kleines Code-Generierungs-Tool, das aus den Symfony-Routen eine TypeScript-Datei mit Hilfsfunktionen erzeugt, sodass Routen-Änderungen im Backend sofort als Compile-Fehler im Frontend sichtbar werden, statt erst zur Laufzeit als kaputter Link aufzufallen.

5. Formulare und Validierungsfehler an React weiterreichen

Formulare sind der Bereich, in dem Symfony mit Inertia.js seine größte Stärke gegenüber einer reinen API-Architektur zeigt. Der Inertia-Frontend-Client stellt einen useForm-Hook bereit, der Formulardaten hält, den Submit per POST an die Symfony-Route sendet und bei einer Validierungsfehler-Response automatisch die Fehler in ein errors-Objekt einsortiert, ohne dass eigener Fehlerbehandlungscode im Frontend geschrieben werden muss.

Auf Symfony-Seite genügt es, bei einer fehlgeschlagenen Validierung mit dem regulären Symfony-Validator eine Inertia-spezifische Fehler-Response zurückzugeben, die die Feld-zu-Fehlermeldung-Zuordnung als assoziatives Array enthält. Inertia erkennt diese Antwort automatisch am HTTP-Statuscode 422 und aktualisiert die Formular-Fehler im React-Zustand, ohne die aktuelle Seite zu verlassen. Für Symfony mit Inertia.js bedeutet das: Dieselbe Validierungslogik, die auch bei klassischen Twig-Formularen genutzt wird, funktioniert unverändert weiter.


// resources/js/Pages/Orders/Create.tsx
import { useForm } from '@inertiajs/react';

export default function Create() {
    // Inertia's useForm tracks data, errors and submission state together
    const { data, setData, post, processing, errors } = useForm({
        customerName: '',
        total: 0,
    });

    function submit(event: React.FormEvent) {
        event.preventDefault();
        post('/orders'); // Sends a POST to the Symfony route, errors flow into `errors`
    }

    return (
        <form onSubmit={submit}>
            <input
                value={data.customerName}
                onChange={(e) => setData('customerName', e.target.value)}
            />
            {errors.customerName && <span>{errors.customerName}</span>}
            <button type="submit" disabled={processing}>Speichern</button>
        </form>
    );
}

6. Geteilte Daten: Auth-Status auf jeder Seite verfügbar machen

Nicht jede Information, die eine React-Seite benötigt, sollte aus jedem einzelnen Controller heraus wiederholt werden, etwa der Name des eingeloggten Benutzers oder eine ungelesene Benachrichtigungs-Zahl. Symfony mit Inertia.js löst das über geteilte Props, die zentral an einer Stelle definiert werden und automatisch jeder Inertia-Response beigemischt werden, unabhängig davon, welcher Controller die Anfrage bearbeitet.

Diese geteilten Daten werden über einen zentralen Middleware-artigen Mechanismus bereitgestellt, der vor jeder Inertia-Response ausgeführt wird. Für Symfony mit Inertia.js bietet sich dafür ein Event-Listener auf kernel.controller oder ein dedizierter Service an, der den aktuellen Security-Token ausliest und die relevanten Benutzerdaten in die geteilten Props einspeist, bevor die eigentliche Controller-Logik läuft.


// src/EventListener/InertiaSharedDataListener.php
namespace App\EventListener;

use Inertia\Inertia;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;

#[AsEventListener(event: KernelEvents::CONTROLLER)]
final class InertiaSharedDataListener
{
    public function __construct(
        private readonly Security $security,
    ) {
    }

    public function __invoke(ControllerEvent $event): void
    {
        $user = $this->security->getUser();

        // Shared props are merged into every Inertia response automatically
        Inertia::share('auth', [
            'user' => $user ? ['name' => $user->getUserIdentifier()] : null,
        ]);
    }
}

7. Partial Reloads: nur benötigte Props nachladen

Eine Seite mit mehreren unabhängigen Datenblöcken, etwa einer Bestellliste und einem separaten Statistik-Widget, muss bei einer Filteränderung nicht zwingend alle Props neu berechnen. Symfony mit Inertia.js unterstützt Partial Reloads, bei denen das Frontend über den only-Parameter gezielt anfordert, welche Props aktualisiert werden sollen. Auf Symfony-Seite lässt sich das über Inertia::lazy() abbilden, wodurch ein teurer Prop-Wert nur berechnet wird, wenn er tatsächlich angefragt wurde.

Dieser Mechanismus ist besonders wertvoll bei rechenintensiven Aggregationen, etwa einer Umsatzstatistik, die über mehrere Tabellen joint. Ohne Partial Reloads würde Symfony mit Inertia.js diese Berechnung bei jeder kleinen Filteränderung erneut ausführen, selbst wenn nur die Bestellliste betroffen ist. Mit Inertia::lazy() wird die teure Berechnung übersprungen, solange das Frontend das entsprechende Prop nicht explizit anfordert.

8. Server-Side Rendering für SEO-relevante Seiten

Standardmäßig rendert Symfony mit Inertia.js die React-Komponenten ausschließlich im Browser, was für Backend-Anwendungen wie Bestellverwaltung oder Kundenportale meist ausreicht, für öffentlich zugängliche, SEO-relevante Seiten aber problematisch ist, weil Suchmaschinen-Crawler ohne ausgeführtes JavaScript nur eine leere Seite sehen. Für diese Fälle unterstützt Inertia optionales Server-Side Rendering über einen separaten Node.js-Prozess, der dieselben React-Komponenten serverseitig zu HTML rendert, bevor Symfony die Antwort ausliefert.

Der Betrieb dieses SSR-Prozesses ist der einzige Punkt, an dem Symfony mit Inertia.js doch wieder eine Node.js-Laufzeit im Produktionsbetrieb benötigt, zusätzlich zu PHP-FPM. In der Praxis wird dieser Node-Prozess als eigener systemd-Service oder Container neben dem PHP-Prozess betrieben, und Symfony leitet Anfragen für SSR-relevante Seiten intern an ihn weiter, bevor das finale HTML an den Browser geht.

9. Symfony mit Inertia.js im Vergleich zu API Platform

Die Entscheidung zwischen Symfony mit Inertia.js und einer vollständigen API-Architektur mit API Platform hängt maßgeblich davon ab, ob das Frontend jemals von einem anderen Client als der eigenen Webanwendung konsumiert werden soll. Die folgende Übersicht stellt die wichtigsten Unterschiede gegenüber.

Aspekt Klassische REST-API + SPA Symfony mit Inertia.js
Separate API-Schicht nötig Ja Nein
Client-Routing dupliziert Backend-Routing Meist ja Nein, Symfony bleibt Quelle
Validierungsfehler-Handling Selbst gebaut Eingebaut über useForm
Externe API-Konsumenten möglich Ja Nein, intern gekoppelt
SEO ohne zusätzlichen Aufwand Abhängig vom Setup Nur mit optionalem SSR-Prozess

Wer eine API benötigt, die auch von einer mobilen App oder externen Partnern konsumiert wird, kommt an einer echten REST- oder GraphQL-Schicht wie API Platform nicht vorbei. Symfony mit Inertia.js ist die richtige Wahl, wenn das Frontend ausschließlich für die eigene Webanwendung existiert und keine zweite Schnittstelle gepflegt werden soll.

Mironsoft

Symfony-Entwicklung mit modernem React-Frontend

SPA-Gefühl ohne eine zweite API pflegen zu müssen?

Wir bauen Symfony-Anwendungen mit Inertia.js und React, inklusive Formular-Validierung, geteilten Daten und optionalem Server-Side Rendering für SEO-relevante Seiten.

Architektur-Beratung

Inertia.js vs. API Platform für euren Anwendungsfall bewerten

Implementierung

Controller, React-Seiten und geteilte Props sauber aufsetzen

SSR-Betrieb

Server-Side Rendering für öffentliche, SEO-relevante Seiten einrichten

10. Zusammenfassung

Symfony mit Inertia.js zu kombinieren bedeutet, Controller weiterhin zentral als einzige Quelle der Wahrheit für Routing, Autorisierung und Datenzugriff zu behalten, während React für die Interaktivität im Browser sorgt. Statt einer separaten JSON-API liefern Controller direkt React-Komponentennamen mit Props, Formulare nutzen den eingebauten useForm-Hook für Validierungsfehler, und geteilte Props sorgen dafür, dass Auth-Status und andere globale Daten nicht in jedem Controller wiederholt werden müssen.

Der größte Kompromiss bei Symfony mit Inertia.js ist der Verzicht auf eine eigenständige, extern konsumierbare API. Wer das nicht braucht, spart erheblichen Wartungsaufwand gegenüber einer klassischen REST-API-plus-SPA-Architektur. Für öffentliche, SEO-relevante Seiten lohnt sich der zusätzliche Aufwand für Server-Side Rendering über einen begleitenden Node.js-Prozess, während interne Anwendungen ohne SSR völlig ausreichend funktionieren.

Symfony mit Inertia.js — Das Wichtigste auf einen Blick

Kein zweites API

Controller liefern Komponentennamen und Props direkt, ohne separate JSON-Ressourcen-Klassen.

Formulare eingebaut

useForm-Hook verarbeitet Validierungsfehler automatisch über den HTTP-Statuscode 422.

Geteilte Props

Auth-Status und globale Daten zentral über einen Event-Listener bereitstellen.

SSR optional

Node.js-Prozess nur für öffentliche, SEO-relevante Seiten nötig, sonst reines Client-Rendering.

11. FAQ: Symfony mit Inertia.js

1Brauche ich eine separate REST-API?
Nein, Controller liefern direkt Komponentennamen mit Props, keine separate JSON-API nötig.
2Zweites Routing-System?
Nein, Symfonys Routing bleibt die einzige Quelle der Wahrheit für URLs.
3Validierungsfehler an React?
Über eine 422-Response, die useForm automatisch in ein errors-Objekt einsortiert.
4Auth-Status auf jeder Seite?
Über geteilte Props, gesetzt durch einen Event-Listener vor jeder Inertia-Response.
5Was sind Partial Reloads?
Gezieltes Nachladen einzelner Props, kombiniert mit Inertia::lazy() für teure Berechnungen.
6Gut für SEO?
Nur mit zusätzlichem Server-Side Rendering über einen separaten Node.js-Prozess.
7Mobile App bedienen?
Nicht direkt, dafür wird zusätzlich eine echte REST- oder GraphQL-API benötigt.
8Unterschied zu API Platform?
API Platform baut eine vollständige API für beliebige Konsumenten, Inertia koppelt enger und spart die API-Schicht.
9Komplette Seite bei Navigation neu geladen?
Nein, Inertia lädt nur neue Props nach und tauscht die Komponente ohne vollständigen Reload.
10Wie typsicher ist die Zusammenarbeit?
Props sind zur Laufzeit reines JSON, TypeScript hilft im Frontend, eine echte End-to-End-Typprüfung braucht zusätzliches Tooling.