Inhaltsverzeichnis
- Klassisches MVC vs. Magento 2 MVC
- Der vollständige Request-Flow: index.php bis HTML
- FrontController::dispatch() im Detail
- Router-Typen und ihre sortOrder
- Eigene Route mit routes.xml erstellen
- Controller Actions in Magento 2.4.8
- Das Layout-Handle-System
- ViewModel vs. Block: Warum ViewModel besser ist
- Zusammenfassung
- FAQ
Klassisches MVC vs. Magento 2 MVC
Das Model-View-Controller Pattern (MVC) ist eines der grundlegendsten Architekturmuster in der Softwareentwicklung. Es trennt eine Anwendung in drei Schichten: Das Model enthält die Daten und Geschäftslogik, der Controller koordiniert die Interaktion zwischen Model und View, und der View ist für die Darstellung zuständig. In klassischen Web-Frameworks lädt der Controller Daten aus dem Model und übergibt sie direkt an den View-Layer. Laravel oder Symfony folgen diesem Muster sehr direkt: return view('template', ['products' => $products]).
Magento 2 implementiert MVC, aber auf eine fundamental andere Weise. Der entscheidende Unterschied: Ein Magento 2 Controller übergibt keine Daten an einen View. Stattdessen gibt er ein ResultInterface-Objekt zurück, das beschreibt, welche Art von Antwort gerendert werden soll — typischerweise eine HTML-Seite, eine JSON-Antwort oder eine Weiterleitung. Die Daten für die HTML-Seite kommen nicht aus dem Controller, sondern aus Block-Klassen oder ViewModels, die im Layout-XML konfiguriert sind.
Diese bewusste Entkopplung hat einen wichtigen Grund: Magento 2 muss es Dritthersteller-Modulen ermöglichen, Seiteninhalte hinzuzufügen oder zu ändern, ohne den Controller-Code zu modifizieren. Ein Modul, das ein Banner auf der Produktdetailseite hinzufügen will, darf nicht den Core-Controller ändern müssen. Das Layout-XML-System ermöglicht es, den Seiteninhalt deklarativ zu definieren und modular zu erweitern, ohne den Controller-Code zu berühren.
Die Schichten im Magento 2 MVC sind deshalb schärfer getrennt als in klassischen Frameworks. Der Controller ist bewusst dünn — er setzt bestenfalls einen Seitentitel und gibt ein Page-Result-Objekt zurück. Der View-Layer ist dreischichtig: Layout-XML definiert die Struktur, Blocks oder ViewModels liefern die Daten, und PHTML-Templates rendern das HTML. Das Front Controller Pattern verbindet diese Schichten durch ein hierarchisches Router-System.
Der vollständige Request-Flow: index.php bis HTML
Wenn ein Browser einen HTTP-Request an einen Magento 2 Shop sendet, beginnt die Reise bei pub/index.php. Diese Datei ist der einzige öffentlich erreichbare PHP-Einstiegspunkt — alle anderen PHP-Dateien liegen außerhalb des Web-Roots. Das ist das Front Controller Pattern in seiner reinsten Form: ein einzelner Einstiegspunkt für alle Requests. pub/index.php initialisiert den Bootstrap-Prozess, der den Magento-Autoloader, den ObjectManager und die DI-Konfiguration aufbaut.
Der Bootstrap ruft Magento\Framework\App\Http::launch() auf, das den eigentlichen Request-Verarbeitungsprozess startet. Http erstellt ein Request-Objekt aus den PHP-Superglobals ($_SERVER, $_GET, $_POST, $_COOKIE) und übergibt dieses an FrontController::dispatch(). Ab hier übernimmt der Front Controller die Kontrolle und iteriert alle registrierten Router.
Jeder Router implementiert RouterInterface mit einer match(RequestInterface $request)-Methode. Diese Methode gibt entweder eine Action-Klasse zurück (wenn der Router den Request matcht) oder null (wenn er den Request nicht verarbeiten kann). Der Front Controller ruft die Router in der Reihenfolge ihrer sortOrder auf und stoppt beim ersten erfolgreichen Match. Die Action-Klasse implementiert ActionInterface mit einer execute()-Methode.
execute() gibt ein ResultInterface-Objekt zurück. Für HTML-Seiten ist das ein Page-Objekt, das die Layout-Konfiguration enthält. Das Layout-System liest die relevanten Layout-XML-Dateien, baut den Block-Baum auf, ruft die Template-Rendering-Methoden auf und schreibt das finale HTML in die HTTP-Response. Diese Response wird dann an den Webserver zurückgegeben und an den Browser gesendet.
FrontController::dispatch() im Detail
Die Methode FrontController::dispatch() ist das Herzstück der Magento 2 Request-Verarbeitung. Sie erhält ein RequestInterface-Objekt und gibt ein ResponseInterface-Objekt zurück. Intern iteriert sie über alle registrierten Router-Klassen in der konfigurierten Reihenfolge. Für jeden Router wird match($request) aufgerufen. Gibt der Router eine Action-Instanz zurück, wird execute() auf der Action aufgerufen und das Ergebnis verarbeitet.
<?php
// Vereinfachte Darstellung von FrontController::dispatch()
// Originale Klasse: Magento\Framework\App\FrontController
namespace Magento\Framework\App;
use Magento\Framework\App\Request\Http as HttpRequest;
use Magento\Framework\App\Response\Http as HttpResponse;
class FrontController implements FrontControllerInterface
{
public function __construct(
private readonly RouterList $routerList,
private readonly HttpResponse $response
) {}
/**
* Dispatch request through router chain — Front Controller pattern.
*/
public function dispatch(RequestInterface $request): ResponseInterface
{
// Iterate routers in sortOrder sequence
foreach ($this->routerList as $router) {
// Each router either matches the request or returns null
/** @var ActionInterface|null $actionInstance */
$actionInstance = $router->match($request);
if ($actionInstance instanceof ActionInterface) {
// Action found — execute it
$result = $actionInstance->execute();
// Process and render result into response
if ($result instanceof ResultInterface) {
$result->renderResult($this->response);
return $this->response;
}
}
}
// No router matched — 404 Not Found
return $this->response->setHttpResponseCode(404);
}
}
Ein wichtiger Aspekt von dispatch() ist die Behandlung von Forward-Requests. Wenn eine Action forward() aufruft, startet die Router-Iteration von vorne mit einem modifizierten Request-Objekt. Das ermöglicht interne Request-Weiterleitungen ohne HTTP-Redirect. Ein typisches Beispiel: Eine Action prüft ob ein Nutzer eingeloggt ist, und forwardet andernfalls zur Login-Seite — ohne dass der Browser eine Weiterleitung sieht.
Router-Typen und ihre sortOrder
Magento 2 kennt mehrere Standard-Router, die in einer definierten Reihenfolge (sortOrder) vom Front Controller aufgerufen werden. Die Reihenfolge ist entscheidend: Der zuerst matchende Router "gewinnt" und der Request wird nicht an weitere Router weitergegeben. Die Router sind in der globalen di.xml des Magento-Frameworks als Array konfiguriert.
Der Base Router (sortOrder 20) verarbeitet alle Standard-Frontend-URLs nach dem Muster frontName/controllerFolder/actionName. Er liest alle registrierten routes.xml-Dateien und sucht nach einem passenden frontName. Wenn ein Match gefunden wird, bestimmt der Base Router die Controller-Klasse aus dem Modulnamen, dem Controller-Ordner und dem Action-Namen. Er erstellt eine Instanz dieser Klasse und gibt sie zurück.
Der CMS Router (sortOrder 60) wird aufgerufen, wenn der Base Router keinen Match findet. Er durchsucht die CMS-Seiten-Tabelle nach einem Eintrag mit dem passenden URL-Key. Wenn eine CMS-Seite gefunden wird, leitet der CMS Router den Request an die Magento\Cms\Controller\Page\View-Action weiter. Der CMS Router ermöglicht es, Seiten über das CMS-Backend zu verwalten ohne explizite routes.xml-Einträge.
Der URL Rewrite Router verarbeitet umgeschriebene URLs aus der url_rewrite-Tabelle. Kategorien, Produkte und CMS-Seiten haben typischerweise SEO-freundliche URLs wie /damen/schuhe, die intern auf /catalog/category/view/id/42 umgeleitet werden. Der URL Rewrite Router konsultiert diese Tabelle und modifiziert den Request entsprechend, bevor er die URL-Auflösung an den Base Router delegiert. Der Default Router (sortOrder 100) ist der Fallback-Router und rendert die 404-Seite.
Eigene Route mit routes.xml erstellen
Um eine eigene Frontend-URL wie /blog/post/view in Magento 2.4.8 zu registrieren, benötigt man eine routes.xml-Datei im etc/frontend/-Verzeichnis des Moduls. Diese Datei registriert einen frontName (der erste Teil der URL) beim Base Router und verknüpft ihn mit dem Modulnamen. Der Base Router verwendet dann den Modulnamen um den Namespace für die Controller-Klassen zu bestimmen.
<?xml version="1.0"?>
<!-- app/code/Mironsoft/Blog/etc/frontend/routes.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<router id="standard">
<!-- frontName: first URL segment — /blog/... -->
<route id="mironsoft_blog" frontName="blog">
<module name="Mironsoft_Blog"/>
</route>
</router>
</config>
Der frontName ist der erste Teil der URL, der vom Base Router gematcht wird. Der id der Route ist der Handle-Prefix für das Layout-System. Mit dieser Konfiguration matched der Router alle URLs die mit /blog/ beginnen und leitet sie an das Modul Mironsoft_Blog weiter. Der zweite URL-Segment bestimmt den Controller-Ordner (z.B. Post für /blog/post/...), der dritte Segment den Action-Namen (z.B. View für /blog/post/view).
Für Admin-Routen gibt es eine äquivalente etc/adminhtml/routes.xml-Datei mit <router id="admin">. Admin-URLs verwenden automatisch den konfigurierten Admin-Pfad (typischerweise /admin/) als Präfix. Admin-Controller müssen zusätzlich ACL-Berechtigungen prüfen, weil Admin-Bereich ohne Authentifizierung nicht zugänglich sein sollte.
Controller Actions in Magento 2.4.8
In Magento 2 ist jede Action eine eigene PHP-Klasse — kein Method in einer Controller-Klasse wie in klassischen MVC-Frameworks. Blog/Index.php und Blog/View.php sind zwei separate Klassen. Das folgt dem Single Responsibility Principle: jede Action-Klasse ist für genau eine URL-Action zuständig. Diese Architektur macht es einfacher, einzelne Actions zu erweitern (durch Plugins) ohne andere Actions zu beeinflussen.
<?php
declare(strict_types=1);
namespace Mironsoft\Blog\Controller\Post;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\View\Result\Page;
use Magento\Framework\View\Result\PageFactory;
use Magento\Framework\Controller\Result\RedirectFactory;
use Mironsoft\Blog\Api\PostRepositoryInterface;
/**
* Blog post view action — renders a single blog post.
*/
class View implements HttpGetActionInterface
{
public function __construct(
private readonly PageFactory $pageFactory,
private readonly RedirectFactory $redirectFactory,
private readonly PostRepositoryInterface $postRepository,
private readonly RequestInterface $request
) {}
/**
* Execute action — render blog post or redirect on error.
*/
public function execute(): Page|\Magento\Framework\Controller\Result\Redirect
{
$postId = (int) $this->request->getParam('id');
if (!$postId) {
return $this->redirectFactory->create()->setPath('blog');
}
try {
$post = $this->postRepository->getById($postId);
} catch (NoSuchEntityException) {
return $this->redirectFactory->create()->setPath('blog')->setHttpResponseCode(302);
}
$page = $this->pageFactory->create();
$page->getConfig()->getTitle()->set($post->getTitle());
// Make post data available to layout blocks/ViewModels via registry alternative
// In Magento 2.4.8: use ViewModel + Repository, not registry
return $page;
}
}
Seit Magento 2.4 ist die Verwendung von HttpGetActionInterface oder HttpPostActionInterface statt AbstractAction die empfohlene Vorgehensweise. Diese Interfaces signalisieren dem Framework, welche HTTP-Methoden die Action akzeptiert. Wenn eine GET-Action mit einem POST-Request aufgerufen wird, gibt das Framework automatisch einen 405 Method Not Allowed zurück. Das ist sicherer und klarer als der alte Weg, bei dem AbstractAction::execute() für alle HTTP-Methoden aufgerufen wurde.
Das Layout-Handle-System
Das Layout-Handle-System ist das Herzstück des View-Layers in Magento 2 und der größte konzeptionelle Unterschied zu klassischen MVC-Frameworks. Ein Layout-Handle ist ein String, der bestimmt, welche Layout-XML-Dateien für eine bestimmte Seite geladen werden. Der Handle wird aus der Route zusammengesetzt: {routeId}_{controllerFolder}_{actionName}, immer in Kleinbuchstaben.
Für die URL /blog/post/view mit der Route-ID mironsoft_blog ergibt sich der Handle mironsoft_blog_post_view. Das Layout-System sucht nach einer Datei view/frontend/layout/mironsoft_blog_post_view.xml im Modul. Zusätzlich werden immer globale Handles wie default und seitenspezifische Handles wie catalog_product_view geladen. Diese Hierarchie ermöglicht es, Elemente global (über default) oder seiten-spezifisch zu definieren.