Was ist Symfony?
Symfony ist zwei Dinge zugleich. Es ist ein Set aus wiederverwendbaren PHP-Komponenten — HttpFoundation, Routing, Console, Mailer, Serializer und Dutzende weitere — und es ist ein vollständiges Web-Framework, das aus diesen Komponenten zusammengesetzt ist. Diese duale Natur erklärt fast alles an Symfony: Das Framework ist stabil, weil seine Einzelteile stabil sind, und diese Teile sind auch unabhängig voneinander nützlich.
Symfony wurde 2005 von Fabien Potencier erstellt, wird von SensioLabs gewartet und ist zum Maschinenraum des modernen PHP geworden. Laravel basiert auf Symfony-Komponenten, Drupal und phpBB hängen von ihnen ab, und Tausende von Bibliotheken nutzen sie, ohne jemals das vollständige Framework zu verwenden.
Symfony verlangt mehr von dir als ein „Batteries-included“-Framework. Im Gegenzug erhältst du ein System, das logisch nachvollziehbar ist und das du über Jahre hinweg erweitern und aktualisieren kannst.
Komponenten: das eigentliche Produkt
Die Komponenten sind das Herzstück des Projekts. Jede einzelne löst ein spezifisches Problem und ist unabhängig vom Framework.
- HttpFoundation — objektorientierte
Request- undResponse-Klassen. - Routing — Zuordnung von URLs zu Controllern mittels Attributen, YAML oder PHP.
- Console — Erstellung von Command-Line-Tools mit Argumenten und Optionen.
- Mailer — Versand von E-Mails über Transports und templated messages.
- Serializer — Konvertierung von Objekten in JSON, XML und zurück.
- EventDispatcher — Entkopplung von Code durch Events und Listener.
Du kannst jede dieser Komponenten direkt installieren:
composer require symfony/http-foundation
use Symfony\Component\HttpFoundation\Request;
$request = Request::createFromGlobals();
$page = $request->query->getInt('page', 1);
Diese Portabilität ist der Grund, warum Symfony-Komponenten überall auftauchen, selbst in Projekten, die niemals das vollständige Framework einsetzen würden.
Der HTTP-Kernel und Controller
In einer vollständigen Anwendung fließt jede Anfrage durch den HttpKernel. Dieser löst ein Event aus, matcht eine Route, ruft einen Controller auf und wandelt den zurückgegebenen Response in eine Ausgabe um. Controller erweitern AbstractController, um praktische Helper-Methoden zu erhalten.
class BlogController extends AbstractController
{
#[Route('/blog/{slug}', name: 'blog_show')]
public function show(string $slug, PostRepository $posts): Response
{
$post = $posts->findOneBy(['slug' => $slug]);
if (!$post) {
throw $this->createNotFoundException('Post not found');
}
return $this->render('blog/show.html.twig', ['post' => $post]);
}
}
Ein Controller muss einen Response zurückgeben. $this->render() erstellt einen solchen aus einem Twig-Template, $this->json() gibt JSON zurück und $this->redirectToRoute() gibt einen Redirect zurück. Da das Response-Objekt explizit ist, sind Testing und middleware unkompliziert.
Twig-Templates erweitern ein Basis-Layout und füllen Blöcke aus; die Ausgabe wird automatisch escaped, sofern man sich nicht explizit dagegen entscheidet:
{# templates/blog/show.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<article>
<h1>{{ post.title }}</h1>
<p>{{ post.publishedAt|date('F j, Y') }}</p>
{{ post.body|raw }}
</article>
{% endblock %}
Attribut-Routing
Routes werden mithilfe von PHP 8 Attributen direkt neben der Action deklariert, die sie verarbeitet.
#[Route('/blog', name: 'blog_index', methods: ['GET'])]
#[Route('/blog/{slug}', name: 'blog_show', methods: ['GET'])]
Das Routing kann alternativ auch in config/routes.yaml definiert werden, wenn Sie es lieber separat halten möchten. Attribute sind in modernem Symfony der Standard, da die Route und die Methode so zusammenbleiben. Sie können jederzeit prüfen, was registriert ist, mit:
php bin/console debug:router
Der Service Container und Autowiring
Fast alles in Symfony ist ein Service, und der Container baut diese auf und verbindet sie miteinander. Wenn Autowiring aktiviert ist, müssen Sie lediglich den Typ im Konstruktor angeben (Type-Hinting).
class PostPublisher
{
public function __construct(
private EntityManagerInterface $em,
private MailerInterface $mailer,
) {}
}
config/services.yaml weist den Container an, Ihre Klassen per Autowiring zu verbinden:
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
exclude:
- '../src/Entity/'
- '../src/Kernel.php'
Wenn zwei Implementierungen eines Interfaces existieren, binden Sie die richtige explizit. Für die Produktion wird der Container in reines PHP kompiliert, weshalb er trotz der dynamischen Schreibweise so schnell ist.
Bundles und Flex-Rezepte
Ein Bundle verpackt Funktionen in eine distribuiertbare Einheit. Funktionalitäten von Drittanbietern – wie Doctrine, Security, Mailer oder Admin-Panels – werden als Bundle bereitgestellt, und auch das Framework selbst setzt sich aus Core-Bundles zusammen.
Die Installation erfolgt mit einem einzigen Befehl:
composer require symfony/orm-pack
Symfony Flex ist das Composer-Plugin, das diesen Prozess vereinfacht. Wenn ein Paket über ein Rezept verfügt, erstellt Flex die Konfigurationsdateien, registriert das Bundle und fügt häufig Umgebungsvariablen oder einen docker-compose-Service hinzu. Aus diesem Grund verfügen moderne Symfony-Apps über eine kleine, explizite Konfiguration anstelle von seitenlangem Boilerplate-Code.
Doctrine ORM
Doctrine ist die standardmäßige Persistence-Layer. Entities sind einfache PHP-Klassen, die mit Mapping-Attributen annotiert sind, während Repositories die Queries kapseln.
#[ORM\Entity(repositoryClass: PostRepository::class)]
class Post
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private string $title;
#[ORM\Column(type: 'text')]
private string $body;
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
private ?\DateTimeImmutable $publishedAt = null;
}
Queries werden in der Regel über die Repository-API oder DQL anstelle von SQL-Strings ausgedrückt:
$latest = $posts->findBy([], ['publishedAt' => 'DESC'], limit: 10);
Schema-Änderungen werden mit Doctrine Migrations verwaltet. Diese vergleichen Ihr Mapping mit der Datenbank und generieren versionierte Migrationsdateien, die Sie vor der Ausführung prüfen können.
Konfiguration und Umgebungen
Symfony trennt die Konfiguration vom Code und wendet sie pro Umgebung an. APP_ENV wählt die Umgebung aus, und die Dateien unter config/ werden hierarchisch aufeinander aufgesetzt.
.enventhält lokale Standardwerte und Umgebungsvariablen.config/packages/konfiguriert Bundles für jede Umgebung.config/packages/dev/undconfig/packages/prod/überschreiben diese pro Umgebung.config/routes.yamlundconfig/services.yamlsind die eigenen Einstellungen der App.
Die dev-Umgebung aktiviert den Profiler und Debug-Tools, test nutzt eine separate Datenbank und prod kompiliert alles für maximale Geschwindigkeit. Das Leeren des Caches nach Konfigurationsänderungen ist ein normaler Teil des Workflows:
php bin/console cache:clear
Messenger und asynchrone Verarbeitung
Die Messenger-Komponente verschiebt die Verarbeitung aus dem Request-Zyklus heraus. Sie senden eine Nachricht an einen Bus und ein Handler verarbeitet diese – entweder sofort oder über einen Worker.
final class UserRegistered
{
public function __construct(public readonly int $userId) {}
}
#[AsMessageHandler]
final class SendWelcomeEmail
{
public function __invoke(UserRegistered $message): void
{
// send the email here
}
}
Senden Sie die Nachricht von überall aus und arbeiten Sie die Queue in einem separaten Prozess ab:
$bus->dispatch(new UserRegistered($user->getId()));
php bin/console messenger:consume async
Als Transports können Doctrine, Redis, AMQP oder ein Drittanbieter-Service genutzt werden. Fehlgeschlagene Nachrichten können erneut versucht oder gespeichert werden, wodurch die Hintergrundverarbeitung zu einem zentralen Bestandteil der Architektur wird.
Testen mit PHPUnit
Symfony wird mit PHPUnit und einer WebTestCase Basisklasse ausgeliefert, die den Kernel bootet und echte HTTP requests ausführt.
class BlogControllerTest extends WebTestCase
{
public function testShowPost(): void
{
$client = static::createClient();
$client->request('GET', '/blog/hello-world');
$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('h1', 'Hello world');
}
}
Da Services injiziert werden, können Unit-Tests eine Abhängigkeit durch einen Stub oder einen Mock ersetzen, ohne den Container zu verändern. Nutzen Sie Integration-Tests für den HTTP layer und Unit-Tests für die Services, die die Business-Logik enthalten.
Best Practices
- Bevorzugen Sie Constructor Injection gegenüber dem Abrufen von Services aus dem Container.
- Halten Sie Controller schlank; verschieben Sie die Logik in Services.
- Nutzen Sie Repositories für Abfragen und halten Sie SQL aus den Controllern heraus.
- Deklarieren Sie Routen als Attribute direkt neben der Action, die sie aufrufen.
- Überlassen Sie Flex die Verwaltung der Bundle-Konfiguration, anstatt Boilerplate-Code zu schreiben.
- Trennen Sie die Konfiguration nach Umgebung und speichern Sie Secrets in Umgebungsvariablen.
- Lagern Sie zeitintensive Aufgaben in den Messenger aus, anstatt sie direkt im Request zu verarbeiten.
- Nutzen Sie den Profiler, wenn etwas langsam ist oder unerwartete Ergebnisse liefert.
Häufige Fehler
- Aufruf von
$container->get()anstatt Dependency Injection zu nutzen. - Platzierung von Business-Logik in Controllern und Entities.
- Vergessen, ein Interface zu registrieren oder zu binden, und sich anschließend fragen, was der Container aufgelöst hat.
- Committen von
.envSecrets, anstatt sie aus der Versionsverwaltung fernzuhalten. - Bearbeiten von generierten Migrationen, nachdem diese bereits ausgeführt wurden.
- Willkürliches Mischen von Konfigurationsstilen über YAML, XML und PHP hinweg.
- Ignorieren von Deprecation-Hinweisen bis zum nächsten Major-Upgrade.
Wie geht es weiter?
Symfony bietet Ihnen die Komponenten und die Disziplin, um zukunftssichere Systeme zu bauen, und seine Konzepte finden sich in Frameworks im gesamten Ökosystem wieder. Lesen Sie den Laravel-Guide, um zu sehen, wie diese Komponenten für eine schnelle Anwendungsentwicklung genutzt werden, oder vergleichen Sie die Philosophie mit dem minimalistischen Ansatz von Express. Um die von Ihnen bereitgestellten APIs zu entwerfen und weiterzuentwickeln, fahren Sie mit REST und API-Versionierung fort.