Front Controller Pattern: Von index.php bis zum Controller

· Lesezeit: ca. 15 Minuten · Kategorie: Magento 2 · Architektur

HTTP
Router
Magento 2 · Deep Dive · Routing

Front Controller Pattern:
Von index.php bis zum Controller

Wie verarbeitet Magento 2 jeden HTTP-Request? Bootstrap, FrontController, Router-Kette, URL-Rewriting und Action-Klassen – der komplette Weg vollständig erklärt.

⏱ 15 Min. Deep Dive Routing PHP 8.4

Das Gateway zu jeder Magento-Seite

Jeder HTTP-Request an einen Magento-Shop – ob Produktseite, Checkout oder REST-API-Aufruf – beginnt an exakt einer Stelle: pub/index.php. Von dort aus entscheidet Magento in Millisekunden, welcher Code für diesen Request ausgeführt wird. Das Herzstück dieses Mechanismus ist das Front Controller Pattern.

Das Front Controller Pattern ist kein Magento-spezifisches Konzept. Es stammt aus den GoF-Entwurfsmustern (Gang of Four, 2002) und beschreibt einen einzigen Einstiegspunkt, der alle eingehenden Requests zentralisiert, bevor er sie an die zuständigen Handler weiterleitet. Viele Frameworks implementieren es – aber Magento macht es besonders komplex, weil zwischen dem FrontController und der eigentlichen Controller-Action ein mehrstufiges Router-System liegt.

Dieser Deep Dive erklärt den vollständigen Weg: vom ersten Byte des HTTP-Requests bis zur Instanziierung der Action-Klasse – und wie du dieses System mit eigenen Routern erweiterst.

1. Das Front Controller Pattern: Theorie und Motivation

Das Front Controller Pattern adressiert ein zentrales Problem in Web-Applikationen: Ohne zentralen Einstiegspunkt müsste jede PHP-Datei im Webroot selbst Bootstrap-Logik, Security-Checks und Routing-Code enthalten. Das führt zu massiver Code-Duplikation.

Die Lösung: Ein einziger Einstiegspunkt für alle Requests. Dieser Einstiegspunkt übernimmt alle Cross-Cutting-Concerns und delegiert dann an spezialisierte Handler.


OHNE Front Controller Pattern (Anti-Pattern):
  /catalog/product.php   ← eigener Bootstrap, eigene Auth-Prüfung
  /checkout/cart.php     ← eigener Bootstrap, eigene Auth-Prüfung
  /customer/account.php  ← eigener Bootstrap, eigene Auth-Prüfung
  /api/products.php      ← eigener Bootstrap, eigene Auth-Prüfung

MIT Front Controller Pattern (Magento-Ansatz):
  pub/index.php           ← EINZIGER Einstiegspunkt
      ↓
  FrontController         ← Cross-Cutting-Concerns
      │ ├── Sicherheits-Checks
      │ ├── Session-Management
      │ ├── Area-Bestimmung (frontend / adminhtml / api)
      │ └── CSRF-Schutz
      ↓
  Router-Chain            ← Routing
      ↓
  Konkreter Handler       ← Business Logic (nur ein Handler, keine Duplikation)

In Magento kommt ein weiterer Vorteil hinzu: Der FrontController ist im DI-Container registriert und kann über Plugins erweitert werden. Du kannst Request-Preprocessing oder Response-Postprocessing hinzufügen, ohne pub/index.php anzufassen.

2. Der Bootstrap-Prozess: Was vor dem FrontController passiert

Bevor der FrontController auch nur eine Zeile Code ausführt, durchläuft Magento einen umfangreichen Bootstrap-Prozess. pub/index.php ist dabei nur der Auslöser:


<?php
// pub/index.php (vereinfacht, Original hat ca. 30 Zeilen)
declare(strict_types=1);

use Magento\Framework\App\Bootstrap;

// 1. Autoloader initialisieren (Composer)
require __DIR__ . '/../app/bootstrap.php';

// 2. Bootstrap-Objekt erstellen
//    Liest: BP (Base Path), MAGE_MODE, MAGE_PROFILER
$bootstrap = Bootstrap::create(BP, $_SERVER);

// 3. Applikations-Objekt erstellen und ausführen
//    Für HTTP-Requests: Magento\Framework\App\Http
$app = $bootstrap->createApplication(\Magento\Framework\App\Http::class);

// 4. run() startet den gesamten Request-Lifecycle
$bootstrap->run($app);

Bootstrap-Sequenz (was Bootstrap::run() auslöst):

pub/index.php
    ↓
Bootstrap::create()
    ├── Composer-Autoloader laden
    ├── Environment-Variablen lesen (MAGE_MODE, etc.)
    └── ObjectManager initialisieren (DI-Container)
            ↓
        DI-Konfiguration laden (di.xml aller Module mergen)
        Preferences und Plugins registrieren

Bootstrap::run(Http::class)
    ↓
Http::launch()
    ├── Area-Konfiguration laden (frontend/adminhtml/...)
    ├── Response-Objekt bereitstellen
    └── FrontController::dispatch($request) aufrufen

Performance-Hinweis: Das Mergen aller di.xml-Dateien im Bootstrap ist die teuerste Operation beim Magento-Start. Im Production-Mode wird das Ergebnis in var/di/ gecacht. Im Developer-Mode passiert es bei jedem Request – deshalb ist Developer-Mode merklich langsamer.

3. FrontController: Implementierung und Dispatch-Loop

Das Interface FrontControllerInterface ist minimal: eine einzige Methode dispatch(RequestInterface $request): ResponseInterface. Die Implementierung in Magento\Framework\App\FrontController enthält die Router-Loop:


<?php
declare(strict_types=1);

// vendor/magento/framework/App/FrontController.php (vereinfacht)
namespace Magento\Framework\App;

use Magento\Framework\App\Response\Http as HttpResponse;
use Magento\Framework\Controller\ResultInterface;

class FrontController implements FrontControllerInterface
{
    public function __construct(
        private readonly RouterListInterface $routerList,
        private readonly HttpResponse $response
    ) {}

    /**
     * Dispatch the request through the router chain.
     * Returns HTTP response — HTML, JSON, redirect, etc.
     */
    public function dispatch(RequestInterface $request): ResponseInterface
    {
        $validCounter = 0;
        $allowedLoop  = 100; // safeguard against infinite redirect loops

        do {
            // Reset routing state for each loop iteration
            $request->setDispatched(false);

            // Walk through ALL registered routers in sortOrder sequence
            foreach ($this->routerList as $router) {
                // Each router tries to match the current request path
                /** @var ActionInterface|null $action */
                $action = $router->match($request);

                if ($action === null) {
                    // This router did not match — try next router
                    continue;
                }

                // Match found — execute the action
                $result = $action->execute();

                // Mark request as dispatched so the loop exits
                $request->setDispatched(true);

                if ($result instanceof ResultInterface) {
                    // Result renders itself into the response object
                    $result->renderResult($this->response);
                }

                break; // stop iterating routers
            }

            ++$validCounter;
        } while (!$request->isDispatched() && $validCounter < $allowedLoop);

        return $this->response;
    }
}

Wichtig: Die do-while-Schleife erlaubt interne Weiterleitungen. Ein Router kann den Request modifizieren (z.B. URL-Rewriting zu einem anderen Pfad) und setDispatched(false) setzen, um die Router-Chain erneut zu starten. Das ist der Mechanismus hinter URL-Rewrites.

4. Die fünf Router-Typen in Magento 2

Magento registriert standardmäßig fünf Router, die in der Reihenfolge ihrer sortOrder durchsucht werden. Der erste Router, der einen Match liefert, gewinnt:


<!-- vendor/magento/module-store/etc/di.xml (vereinfacht) -->
<type name="Magento\Framework\App\RouterList">
    <arguments>
        <argument name="routerList" xsi:type="array">
            <!-- sortOrder bestimmt die Reihenfolge -->
            <item name="admin" xsi:type="array">
                <item name="class" xsi:type="string">Magento\Backend\App\Router\DefaultRouter</item>
                <item name="disable" xsi:type="boolean">false</item>
                <item name="sortOrder" xsi:type="string">1</item>
            </item>
            <item name="robots" xsi:type="array">
                <item name="class" xsi:type="string">Magento\Robots\Controller\Router</item>
                <item name="sortOrder" xsi:type="string">10</item>
            </item>
            <item name="urlrewrite" xsi:type="array">
                <item name="class" xsi:type="string">Magento\UrlRewrite\Controller\Router</item>
                <item name="sortOrder" xsi:type="string">20</item>
            </item>
            <item name="standard" xsi:type="array">
                <item name="class" xsi:type="string">Magento\Framework\App\Router\Base</item>
                <item name="sortOrder" xsi:type="string">30</item>
            </item>
            <item name="cms" xsi:type="array">
                <item name="class" xsi:type="string">Magento\Cms\Controller\Router</item>
                <item name="sortOrder" xsi:type="string">60</item>
            </item>
            <item name="default" xsi:type="array">
                <item name="class" xsi:type="string">Magento\Framework\App\Router\DefaultRouter</item>
                <item name="sortOrder" xsi:type="string">100</item>
            </item>
        </argument>
    </arguments>
</type>
Router sortOrder Matcht
Admin Router 1 Admin-URLs (/admin/...), prüft den konfigurierten Admin-Frontname
URL-Rewrite Router 20 SEO-URLs aus url_rewrite-Tabelle (Produkt-, Kategorie-, CMS-URLs)
Standard Router 30 Technische URLs im Format /frontName/controller/action
CMS Router 60 CMS-Seiten nach URL-Key aus der Datenbank
Default Router 100 Fallback — rendert immer die 404-Seite

5. Standard-Router: frontName, Controller und Action

Der Standard-Router ist der wichtigste Router für eigene Module. Er matcht URLs im Format /frontName/controller/action und mappt sie auf Klassen unter Controller/ im Modul-Verzeichnis.


<!-- app/code/Mironsoft/Blog/etc/frontend/routes.xml -->
<!-- Registriert den frontName für dieses Modul beim Standard-Router -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="standard">
        <route id="mironsoft_blog" frontName="blog">
            <module name="Mironsoft_Blog"/>
        </route>
    </router>
</config>

Mit dieser Konfiguration matcht der Standard-Router alle URLs, die mit /blog/ beginnen. Das Mapping von URL zu Klasse folgt einer strikten Konvention:


URL-zu-Klassen-Mapping (Standard-Router):

URL: /blog/post/view
     │    │     │
     │    │     └── Action:     Controller/Post/View.php
     │    └──────── Controller: Controller/Post/
     └───────────── frontName:  routes.xml (frontName="blog")

Vollständiger Klassenname: Mironsoft\Blog\Controller\Post\View

URL: /blog/post/index  → Mironsoft\Blog\Controller\Post\Index
URL: /blog/index/index → Mironsoft\Blog\Controller\Index\Index
URL: /blog/            → Mironsoft\Blog\Controller\Index\Index (Defaults)

Defaults:
  - Kein Controller in URL → "Index"
  - Keine Action in URL    → "Index"

<?php
// Der Standard-Router tut intern (vereinfacht) Folgendes:
// vendor/magento/framework/App/Router/Base.php

namespace Magento\Framework\App\Router;

class Base implements RouterInterface
{
    /**
     * Try to match the request to a module action class.
     */
    public function match(RequestInterface $request): ?ActionInterface
    {
        // Parse URL path: /frontName/controllerPath/actionName
        $pathParts = explode('/', trim($request->getPathInfo(), '/'));

        $frontName      = $pathParts[0] ?? 'index';
        $controllerPath = $pathParts[1] ?? 'index';
        $actionName     = $pathParts[2] ?? 'index';

        // Find module by frontName (from routes.xml registry)
        $module = $this->routeConfig->getModulesByFrontName($frontName);
        if (empty($module)) {
            return null; // This router does not handle this URL
        }

        // Build class name: Vendor\Module\Controller\ControllerPath\ActionName
        $actionClassName = $this->actionList->get($module, $controllerPath, $actionName);

        if (!$actionClassName || !class_exists($actionClassName)) {
            return null;
        }

        // Set routing parameters on the request for later use
        $request->setModuleName($module);
        $request->setControllerName($controllerPath);
        $request->setActionName($actionName);

        // Instantiate and return the action via DI
        return $this->objectManager->create($actionClassName);
    }
}

6. URL-Rewrite-Router: SEO-URLs auflösen

Produktseiten in Magento haben URLs wie /rotes-t-shirt-xl.html statt /catalog/product/view/id/42. Diese Übersetzung übernimmt der URL-Rewrite-Router, der vor dem Standard-Router ausgeführt wird (sortOrder 20 vs. 30).


URL-Rewrite Mechanismus:

Browser-Request: GET /rotes-t-shirt-xl.html

URL-Rewrite-Router (sortOrder 20):
    ↓
SELECT * FROM url_rewrite WHERE request_path = 'rotes-t-shirt-xl.html'
    ↓
Ergebnis: { target_path: 'catalog/product/view/id/42', redirect_code: 0 }
    ↓
Falls redirect_code = 301 oder 302:
    → HTTP Redirect zurückgeben (externe URL-Änderung)

Falls redirect_code = 0 (internes Rewrite):
    → Request-Path auf 'catalog/product/view/id/42' setzen
    → setDispatched(false) → FrontController startet Router-Loop neu
    → Standard-Router matcht jetzt: catalog/product/view → Catalog\Product\View

Ergebnis für Browser: URL bleibt /rotes-t-shirt-xl.html
                       Intern wird aber catalog/product/view/id/42 ausgeführt

<?php
// Vereinfacht: Magento\UrlRewrite\Controller\Router::match()
namespace Magento\UrlRewrite\Controller;

use Magento\UrlRewrite\Service\V1\Data\UrlRewrite;

class Router implements RouterInterface
{
    /**
     * Look up the request path in url_rewrite table.
     * Modifies request for internal rewrites; returns redirect action for external ones.
     */
    public function match(RequestInterface $request): ?ActionInterface
    {
        // Current URL path (e.g. "rotes-t-shirt-xl.html")
        $requestPath = ltrim($request->getPathInfo(), '/');

        // Database lookup
        $rewrite = $this->urlFinder->findOneByData([
            UrlRewrite::REQUEST_PATH => $requestPath,
            UrlRewrite::STORE_ID     => $this->storeManager->getStore()->getId(),
        ]);

        if ($rewrite === null) {
            return null; // No rewrite found — next router's turn
        }

        // External redirect (301/302)
        if ($rewrite->getRedirectType() > 0) {
            return $this->getRedirectAction($rewrite);
        }

        // Internal rewrite — silently change the path and re-dispatch
        $request->setPathInfo('/' . $rewrite->getTargetPath());
        $request->setAlias(Url::REWRITE_REQUEST_PATH_ALIAS, $requestPath);

        // Trigger re-dispatch loop in FrontController
        $request->setDispatched(false);

        return null; // No action to return — router loop will handle the new path
    }
}

Wichtig: Nach einem internen URL-Rewrite gibt der Router null zurück (kein Action-Objekt) und setzt setDispatched(false). Die do-while-Schleife im FrontController startet dann die Router-Chain erneut – jetzt mit dem neuen, internen Pfad. Der Browser sieht davon nichts.

7. Eigene Router erstellen: RouterInterface implementieren

Für spezielle URL-Strukturen – etwa Vanity-URLs, API-Proxies oder parameterreiche Permalinks – kannst du eigene Router registrieren. Das Interface ist einfach:


<?php
declare(strict_types=1);

namespace Mironsoft\Blog\Controller;

use Magento\Framework\App\ActionFactory;
use Magento\Framework\App\ActionInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\App\RouterInterface;
use Magento\Framework\App\Action\Forward;

/**
 * Custom router: resolves /blog/<slug> URLs to the blog post view action.
 * Handles slugs that don't follow the standard frontName/controller/action pattern.
 */
class Router implements RouterInterface
{
    public function __construct(
        private readonly ActionFactory $actionFactory,
        private readonly PostRepositoryInterface $postRepository
    ) {}

    /**
     * Match /blog/<slug> pattern and forward to the post view action.
     */
    public function match(RequestInterface $request): ?ActionInterface
    {
        $identifier = trim($request->getPathInfo(), '/');

        // Only handle paths that start with "blog/" but have no further segments
        // e.g. /blog/mein-erster-beitrag  (not /blog/post/view)
        if (!preg_match('#^blog/([a-z0-9-]+)$#', $identifier, $matches)) {
            return null;
        }

        $slug = $matches[1];

        // Verify the slug exists
        $post = $this->postRepository->getBySlug($slug);
        if ($post === null) {
            return null; // Pass to next router (CMS, Default/404)
        }

        // Forward to existing action — no redirect, URL stays clean
        $request->setModuleName('mironsoft_blog');
        $request->setControllerName('post');
        $request->setActionName('view');
        $request->setParam('id', $post->getId());
        $request->setDispatched(true);

        // ActionFactory creates an internal forward (re-runs action without HTTP redirect)
        return $this->actionFactory->create(Forward::class);
    }
}

<!-- app/code/Mironsoft/Blog/etc/di.xml -->
<!-- Router im DI-Container registrieren -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Framework\App\RouterList">
        <arguments>
            <argument name="routerList" xsi:type="array">
                <item name="mironsoft_blog" xsi:type="array">
                    <item name="class" xsi:type="string">Mironsoft\Blog\Controller\Router</item>
                    <item name="disable" xsi:type="boolean">false</item>
                    <!-- sortOrder 25: nach URL-Rewrite (20), vor Standard-Router (30) -->
                    <item name="sortOrder" xsi:type="string">25</item>
                </item>
            </argument>
        </arguments>
    </type>
</config>

Die Wahl der sortOrder ist entscheidend. Eigene Router sollten nach dem URL-Rewrite-Router (20) und vor dem Standard-Router (30) stehen, damit sie spezifische URLs abfangen, bevor der generische Fallback greift.

8. Action-Klassen: HttpGetActionInterface und Co.

Seit Magento 2.3 gibt es HTTP-Method-Interfaces, die besser als die ältere Action-Basisklasse sind. Sie ermöglichen es Magento, falsche HTTP-Methoden (z.B. POST auf eine GET-only-Route) automatisch mit 405 Method Not Allowed abzuweisen:


<?php
declare(strict_types=1);

namespace Mironsoft\Blog\Controller\Post;

use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Controller\ResultInterface;
use Magento\Framework\View\Result\PageFactory;
use Mironsoft\Blog\Api\PostRepositoryInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Controller\Result\RedirectFactory;

/**
 * GET action: displays a single blog post.
 * Only responds to HTTP GET — POST/PUT/DELETE get 405 automatically.
 */
class View implements HttpGetActionInterface
{
    public function __construct(
        private readonly PageFactory $pageFactory,
        private readonly RequestInterface $request,
        private readonly PostRepositoryInterface $postRepository,
        private readonly RedirectFactory $redirectFactory
    ) {}

    public function execute(): ResultInterface
    {
        $postId = (int) $this->request->getParam('id');

        try {
            $this->postRepository->getById($postId);
        } catch (NoSuchEntityException) {
            return $this->redirectFactory->create()->setPath('noroute');
        }

        $page = $this->pageFactory->create();
        $page->getConfig()->getTitle()->set(__('Blog'));

        return $page;
    }
}

<?php
declare(strict_types=1);

namespace Mironsoft\Blog\Controller\Post;

use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\App\CsrfAwareActionInterface;
use Magento\Framework\App\Request\InvalidRequestException;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Controller\ResultInterface;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Framework\App\Action\Context;

/**
 * POST action: saves a blog comment.
 * Implements CsrfAwareActionInterface for CSRF token validation.
 */
class Comment implements HttpPostActionInterface, CsrfAwareActionInterface
{
    public function __construct(
        private readonly JsonFactory $jsonFactory,
        private readonly RequestInterface $request
    ) {}

    /**
     * Validate CSRF token — called automatically by Magento before execute().
     */
    public function validateForCsrf(RequestInterface $request): ?bool
    {
        // Return null to use Magento's default CSRF validation
        return null;
    }

    /**
     * Create CSRF validation exception — called when CSRF validation fails.
     */
    public function createCsrfValidationException(RequestInterface $request): ?InvalidRequestException
    {
        $response = $this->jsonFactory->create();
        $response->setData(['error' => true, 'message' => 'Invalid form key.']);

        return new InvalidRequestException($response);
    }

    public function execute(): ResultInterface
    {
        $result = $this->jsonFactory->create();
        // ... process comment
        return $result->setData(['success' => true]);
    }
}
Interface HTTP-Methode Typischer Einsatz
HttpGetActionInterface GET Seiten anzeigen, Daten laden
HttpPostActionInterface POST Formulare verarbeiten, Daten speichern
HttpPutActionInterface PUT REST-Updates (selten im Frontend)
HttpDeleteActionInterface DELETE REST-Löschoperationen

9. Das RequestInterface: URL, Parameter und mehr

Das RequestInterface-Objekt ist der zentrale Datenspeicher für alle Request-Informationen. Es ist während des gesamten Request-Lifecycle verfügbar und wird vom Router mit Routing-Informationen befüllt:


<?php
declare(strict_types=1);

namespace Mironsoft\Blog\Controller\Post;

use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Controller\ResultInterface;
use Magento\Framework\View\Result\PageFactory;

/**
 * Demonstrates RequestInterface methods available in actions.
 */
class Demo implements HttpGetActionInterface
{
    public function __construct(
        private readonly RequestInterface $request,
        private readonly PageFactory $pageFactory
    ) {}

    public function execute(): ResultInterface
    {
        // --- URL-Informationen ---
        $pathInfo    = $this->request->getPathInfo();     // "/blog/post/view"
        $moduleName  = $this->request->getModuleName();   // "mironsoft_blog"
        $controller  = $this->request->getControllerName(); // "post"
        $action      = $this->request->getActionName();   // "view"

        // --- Query-Parameter (?id=42&tab=comments) ---
        $id  = (int) $this->request->getParam('id');
        $tab = $this->request->getParam('tab', 'default'); // mit Default-Wert

        // --- POST-Daten ---
        $postData = $this->request->getPost();         // alle POST-Werte als Array
        $email    = $this->request->getPost('email');  // einzelner POST-Wert

        // --- HTTP-Methode ---
        $method  = $this->request->getMethod();   // "GET", "POST", etc.
        $isAjax  = $this->request->isAjax();      // X-Requested-With: XMLHttpRequest

        // --- Header ---
        $accept  = $this->request->getHeader('Accept');
        $referer = $this->request->getHeader('Referer');

        // --- Routing-Status ---
        $dispatched = $this->request->isDispatched(); // true während Ausführung

        return $this->pageFactory->create();
    }
}

Sicherheitshinweis: getParam() liefert sowohl GET- als auch POST-Parameter. Für POST-Formulare immer getPost() verwenden und zusätzlich CsrfAwareActionInterface implementieren. Alle Werte aus getParam() sind unsanitisiert – im Template immer über $block->escapeHtml() ausgeben.

Mironsoft

Magento 2 Routing & Architektur

Routing-Probleme in Magento lösen?

Wir analysieren und erweitern Magento-Routing: Custom-Router für komplexe URL-Strukturen, URL-Rewrite-Optimierung und vollständige Router-Diagnose – nach aktuellen Best Practices.

Router-Analyse
Bestehende Routing-Konfiguration prüfen: Konflikte zwischen Routern, fehlerhafte sortOrder, nicht-erreichbare Actions diagnostizieren.
Custom-Router
Eigene Router für spezifische URL-Muster entwickeln: Slug-basierte URLs, Vanity-URLs, API-Proxies und parameterreiche Permalinks.
Request-Optimierung
Performance-Engpässe im Request-Flow identifizieren: Bootstrap-Optimierung, Router-Overhead reduzieren, URL-Rewrite-Cache tunen.

10. Zusammenfassung

Das Front Controller Pattern in Magento 2 ist ein mehrstufiges System: pub/index.php triggert den Bootstrap, der Bootstrap erstellt das HTTP-Applikations-Objekt, das den FrontController::dispatch() aufruft. Dieser durchsucht in einer do-while-Schleife eine sortierte Liste von Routern. Der erste Router, der matcht, liefert eine Action-Klasse zurück, die execute() ausführt und ein Result-Objekt zurückgibt.

Front Controller Pattern – Überblick

Bootstrap

pub/index.php initialisiert Composer-Autoloader und DI-Container. Im Production-Mode gecacht in var/di/. Teuerste Phase — einmalig pro Request.

Router-Chain

Admin (1) → URL-Rewrite (20) → Standard (30) → CMS (60) → Default/404 (100). Eigene Router via di.xml mit gewähltem sortOrder einklinken.

URL-Rewrite

Übersetzt SEO-URLs (/rotes-shirt.html) in technische Pfade (catalog/product/view/id/42). Internes Rewrite: URL bleibt, Pfad ändert sich. Externes Rewrite: HTTP 301/302.

Action-Klassen

HttpGetActionInterface statt alter Action-Basisklasse. Magento validiert HTTP-Methode automatisch — falsche Methoden → 405. CsrfAwareActionInterface für POST-Formulare.

11. FAQ: Front Controller Pattern in Magento 2

1 FrontController vs. normaler Controller: Was ist der Unterschied?
FrontController: Dispatcher – empfängt alle HTTP-Requests und sucht über die Router-Chain den zuständigen Handler. Keine Business-Logik. Normaler Controller: konkreter Request-Handler für eine spezifische URL, implementiert HttpGetActionInterface, enthält die eigentliche Ausführungslogik.
2 Warum beginnt jeder Request bei pub/index.php?
Nur pub/ ist als Webroot konfiguriert. Nginx/Apache leitet alle Requests, die keine statische Datei treffen, per try_files an pub/index.php weiter. Das ist das Front Controller Pattern: ein einziger Einstiegspunkt für alle dynamischen Requests.
3 In welcher Reihenfolge werden Magento-Router durchsucht?
Admin (1) → URL-Rewrite (20) → Standard (30) → CMS (60) → Default/404 (100). Eigene Router via di.xml mit gewähltem sortOrder einfügen. sortOrder 25 ist der typische Platz für Custom-Router (nach URL-Rewrite, vor Standard).
4 Wie registriert man einen eigenen Router?
1. Klasse mit RouterInterface (match()-Methode) erstellen. 2. In etc/di.xml beim Typ Magento\Framework\App\RouterList registrieren – als Item mit class, disable=false und sortOrder. Danach: setup:di:compile + Cache-Flush.
5 HttpGetActionInterface vs. alte Action-Basisklasse?
Die alte Action-Basisklasse antwortete auf alle HTTP-Methoden. HttpGetActionInterface beschränkt auf GET — ein POST-Request ergibt automatisch 405 Method Not Allowed ohne manuellen Code. Verbessert Sicherheit und REST-Compliance.
6 Wie debuggt man den Routing-Status in Magento?
Xdebug-Breakpoint in FrontController::dispatch() · Plugin auf RouterInterface::match() zum Loggen · bin/magento dev:profiler:enable mit HTML-Profiler · Query-Log für url_rewrite-Lookups · Developer-Mode für detaillierte Fehlermeldungen.
7 Warum gibt es URL-Rewrite-Router UND Standard-Router?
Standard-Router: matcht technische URLs direkt – kein DB-Lookup, sehr schnell. URL-Rewrite-Router: übersetzt SEO-URLs per DB-Lookup. Trennung erlaubt es, SEO-URLs im Admin zu ändern ohne Code anzufassen. Technische URLs funktionieren auch ohne URL-Rewrite-Einträge.
8 Wie funktioniert das Routing in der Admin-Area?
Der Admin-Router (sortOrder 1) prüft den konfigurierten Admin-Frontname (Standard: /admin/, in etc/env.php änderbar). Admin-Actions in etc/adminhtml/routes.xml registrieren. Admin-Actions erben von Magento\Backend\App\Action – inkl. Auth + ACL-Checks.
9 Was passiert wenn kein Router matcht?
Der Default-Router (sortOrder 100) matcht immer – er liefert die NoRoute/404-Action. Die do-while-Schleife im FrontController verlässt sich darauf: ohne Default-Router gäbe es eine Endlosschleife. Die 404-Seite ist über noroute_index_index.xml konfigurierbar.
10 Wie teste ich einen Custom-Router mit PHPUnit?
Im Unit-Test: RequestInterface mocken, getPathInfo() auf Testpfad setzen, Repository mocken. Dann router->match($request) aufrufen – prüfen ob null (kein Match) oder ActionInterface zurückkommt. Für Integrationstests: Magento\TestFramework\Request mit echten DB-Fixtures.