MVC in Magento 2: Warum es kein klassisches MVC ist – Architektur erklärt | Mironsoft
AI generated

MVC in Magento 2: Warum es kein klassisches MVC ist

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

MVC
Layout
Magento 2 · Deep Dive · Architektur

MVC in Magento 2:
Warum es kein klassisches MVC ist

Magentos Architektur nennt sich MVC – funktioniert aber fundamental anders. Request-Flow, FrontController, Layout-XML als vierte Schicht, Block-System und ViewModel: vollständig erklärt.

⏱ 14 Min. Deep Dive Architektur PHP 8.4

Was Magento 2 MVC nennt – und was es wirklich ist

Jede Magento-Dokumentation beginnt irgendwo mit dem Satz: „Magento 2 basiert auf dem MVC-Pattern." Das stimmt – und stimmt gleichzeitig nicht. Wer klassisches MVC kennt (wie es Ruby on Rails, Laravel oder Symfony implementiert) und dann Magento-Code zum ersten Mal sieht, ist verwirrt: Wo sind die Views? Warum gibt es XML-Dateien für das Layout? Was macht dieser FrontController? Was ist ein Block?

Die Wahrheit: Magento 2 hat MVC als Ausgangspunkt genommen und es um mehrere Schichten erweitert, die in anderen Frameworks nicht existieren. Das Ergebnis ist eine Architektur, die näher an einem HMVC (Hierarchical MVC) oder sogar einem MVVM-Muster mit Layout-Orchestrierung liegt als am klassischen MVC.

Dieser Deep Dive erklärt, was in Magento wirklich passiert – von der HTTP-Request-Ankunft bis zur HTML-Ausgabe – und warum diese Architektur für ein Enterprise-E-Commerce-System sinnvoll ist.

1. Klassisches MVC – kurze Wiederholung

Klassisches MVC – wie von Trygve Reenskaug 1979 entworfen und von Web-Frameworks wie Rails oder Symfony implementiert – teilt eine Anwendung in drei klar getrennte Schichten:

  • Model: Hält die Daten und die Geschäftslogik. Kommuniziert mit der Datenbank. Kennt weder View noch Controller.
  • View: Rendert die Daten für den Benutzer. Kennt das Model (oder zumindest seine Daten), kennt aber keinen Controller.
  • Controller: Empfängt die Request, ruft das Model ab und gibt die Daten an die View weiter. Steuert den Ablauf.

<?php
// Klassisches MVC in einem einfachen Framework (z.B. Laravel-Stil):

// CONTROLLER: Empfängt Request, lädt Model, gibt an View weiter
class ProductController
{
    public function show(int $id): Response
    {
        $product = Product::findOrFail($id); // Model laden
        return view('products.show', ['product' => $product]); // View rendern
    }
}

// MODEL: Daten und Logik
class Product extends Model
{
    protected $table = 'products';
    public function getDiscountPrice(): float { ... }
}

// VIEW: resources/views/products/show.blade.php
// Direkte Datenbindung, kein Layer dazwischen
// <h1>{ { $product->name } }</h1>
// <p>{ { $product->getDiscountPrice() } }</p>

Das ist klar, simpel und in drei Zeilen erklärbar. Magento macht das anders – und mit gutem Grund.

2. Der Magento Request-Flow: Von index.php bis zur Antwort

In Magento beginnt jeder Request bei pub/index.php. Was dann passiert, ist erheblich komplexer als in klassischen Frameworks:


HTTP Request
    ↓
pub/index.php
    ↓ Bootstrap (Autoloading, DI-Container initialisieren)
Magento\Framework\App\Bootstrap::run()
    ↓
Magento\Framework\App\Http (Application-Objekt)
    ↓
FrontController::dispatch()
    ↓
Router-Liste durchsuchen (Standard, CMS, Admin, URL-Rewrite...)
    ↓
Passendem Controller-Action gefunden
    ↓
ActionInterface::execute() → ResultInterface zurückgeben
    ↓
Result::renderResult() → Layout initialisieren
    ↓
Layout::loadXml() → Alle layout/*.xml-Dateien mergen
    ↓
Layout::generateXml() → Block-Struktur aufbauen
    ↓
Layout::generateElements() → Block-Objekte instanziieren
    ↓
Layout::getOutput() → Block::toHtml() rekursiv aufrufen
    ↓
HTTP Response mit HTML
    ↓
Browser

Schon in dieser Übersicht sieht man: Zwischen Controller und der finalen HTML-Ausgabe liegen mehrere Schichten, die in klassischem MVC nicht existieren: Result-Objekte, Layout-XML-Merging, Block-Baum-Aufbau. Das ist der fundamentale Unterschied.

3. FrontController und Router: Magentos Dispatcher

Der FrontController in Magento entspricht nicht dem „Controller" in MVC. Er ist ein Dispatcher – er ist dafür zuständig, den Request an den richtigen Controller weiterzuleiten, indem er eine Liste von Routern durchsucht.


<?php
// Vereinfacht: Was FrontController::dispatch() macht
// vendor/magento/framework/App/FrontController.php

namespace Magento\Framework\App;

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

    public function dispatch(RequestInterface $request): ResponseInterface
    {
        // Iterate through all registered routers
        foreach ($this->routerList as $router) {
            // Each router tries to match the request to an action
            $action = $router->match($request);

            if ($action instanceof ActionInterface) {
                // Found! Execute the action.
                $result = $action->execute();

                // Result is NOT HTML yet — it's a ResultInterface object
                // (ResultPage, ResultJson, ResultRedirect...)
                if ($result instanceof ResultInterface) {
                    $result->renderResult($response);
                }

                return $response;
            }
        }

        // No router matched — 404
        throw new NotFoundException(__('Page not found.'));
    }
}

Magento registriert standardmäßig folgende Router in dieser Reihenfolge:

  • Base Router: Prüft Admin-Routen (adminhtml)
  • Standard Router: Matcht frontName/controller/action (z.B. catalog/product/view)
  • CMS Router: Matcht CMS-Seiten via URL-Key aus der Datenbank
  • URL Rewrite Router: Prüft url_rewrite-Tabelle für SEO-URLs
  • Default Router: Rendert die 404-Seite

4. Der Controller in Magento 2

Ein Magento-Controller ist eine Klasse, die ActionInterface implementiert – genauer gesagt, eine Klasse unter Controller/ in einem Modul, die von Magento\Framework\App\Action\Action erbt. Seine Aufgabe ist bewusst minimalistisch: einen Request entgegennehmen und ein Result-Objekt zurückgeben.


<?php
declare(strict_types=1);

namespace Mironsoft\Blog\Controller\Post;

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

/**
 * Controller action: renders a single blog post page.
 */
class View implements HttpGetActionInterface
{
    public function __construct(
        private readonly PageFactory $pageFactory,
        private readonly RequestInterface $request,
        private readonly PostRepositoryInterface $postRepository,
        private readonly RedirectFactory $redirectFactory
    ) {}

    /**
     * Execute: validate post exists, return ResultPage.
     * The controller does NOT render HTML — it returns a Result object.
     */
    public function execute(): ResultInterface
    {
        $postId = (int) $this->request->getParam('id');

        try {
            $post = $this->postRepository->getById($postId);
        } catch (NoSuchEntityException) {
            // Return redirect result — controller decides the RESPONSE TYPE, not HTML
            $redirect = $this->redirectFactory->create();
            return $redirect->setPath('noroute');
        }

        // ResultPage tells Magento: render a full HTML page
        // The actual content is defined by Layout-XML, NOT by the controller
        $page = $this->pageFactory->create();
        $page->getConfig()->getTitle()->set($post->getTitle());

        return $page;
    }
}

Kernunterschied zu klassischem MVC: Der Magento-Controller gibt kein fertig gerendertes HTML zurück – er gibt ein ResultPage-Objekt zurück. Was auf der Seite angezeigt wird, entscheidet nicht der Controller, sondern das Layout-XML. Das ist der fundamentale Unterschied.

5. Layout-XML: Die vierte Schicht die MVC sprengt

Das Layout-XML-System ist das Element in Magentos Architektur, das am stärksten von klassischem MVC abweicht. In anderen Frameworks entscheidet der Controller, welches Template gerendert wird: return view('blog.post.show', $data). In Magento 2 entscheidet das Layout-XML – eine XML-Konfigurationsschicht, die deklarativ beschreibt, welche Blöcke mit welchen Templates auf welcher Seite erscheinen.


<!-- app/code/Mironsoft/Blog/view/frontend/layout/mironsoft_blog_post_view.xml -->
<!-- Handle-Name = frontName_controller_action = mironsoft_blog_post_view -->
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"
      layout="1column">
    <body>
        <!-- Add a block to the 'main' container -->
        <referenceContainer name="main">
            <block class="Magento\Framework\View\Element\Template"
                   name="mironsoft.blog.post.view"
                   template="Mironsoft_Blog::post/view.phtml">
                <arguments>
                    <!-- ViewModel injizieren — kein Controller-Code nötig -->
                    <argument name="view_model" xsi:type="object">
                        Mironsoft\Blog\ViewModel\PostViewModel
                    </argument>
                </arguments>
            </block>
        </referenceContainer>

        <!-- Breadcrumbs anpassen -->
        <referenceBlock name="breadcrumbs">
            <action method="addCrumb">
                <argument name="crumbName" xsi:type="string">blog</argument>
                <argument name="crumbInfo" xsi:type="array">
                    <item name="label" xsi:type="string" translate="true">Blog</item>
                    <item name="link" xsi:type="string">/blog</item>
                </argument>
            </action>
        </referenceBlock>
    </body>
</page>

Magento mergt beim Seitenaufbau alle Layout-XML-Dateien für den aktuellen Handle. Das erlaubt es mehreren Modulen, dieselbe Seite zu modifizieren – ohne sich zu kennen, ohne dass der Controller geändert werden muss. Das ist in klassischem MVC nicht vorgesehen.

Layout-Handles: Jede Magento-Seite hat mehrere aktive Layout-Handles gleichzeitig: default (für alle Seiten), catalog_product_view (für alle Produktseiten) und catalog_product_view_type_simple (nur für Simple Products). Alle passenden XML-Dateien werden gemergt und ergeben die finale Block-Struktur.

6. Block und Template: Magentos View-Schicht

In Magentos View-Schicht gibt es zwei Komponenten, die zusammenarbeiten: Block-Klassen (PHP) und phtml-Templates (PHP-HTML-Mix). In modernem Magento-Code kommt noch der ViewModel dazu (mehr dazu im nächsten Abschnitt).


<?php
// BLOCK: Bindung an das Layout-System
// Erbt von Template, verwaltet Caching, Child-Blocks, Template-Zuweisung
namespace Magento\Catalog\Block\Product;

class View extends \Magento\Framework\View\Element\Template
{
    /**
     * Block knows its template (set via Layout-XML or constructor).
     * It doesn't render itself — it provides data to the template.
     */
    public function getProduct(): ProductInterface
    {
        // Data is NOT passed from the controller
        // The Block fetches it itself via the registry or repository
        return $this->coreRegistry->registry('current_product');
    }
}

<?php
// TEMPLATE (phtml): Die eigentliche HTML-Ausgabe
// Verfügbar: $block (Block-Objekt), $viewModel (ViewModel)

/** @var \Magento\Catalog\Block\Product\View $block */
$product = $block->getProduct();
?>
<div class="product-view">
    <h1 class="product-name"><?= $block->escapeHtml($product->getName()) ?></h1>
    <div class="product-price"><?= /* ... */ ?></div>
</div>

Im Unterschied zu klassischem MVC: Das Template empfängt seine Daten nicht vom Controller. Der Controller gibt nur ein ResultPage-Objekt zurück. Das Template holt sich seine Daten aus dem Block, der Block holt sie aus dem Registry, Repository oder anderen Services.

7. Das Model in Magento: EAV, Repository und mehr

Das „Model" in Magento ist keine einzelne Klasse – es ist ein Schichtsystem aus mindestens drei Komponenten:


Magento Model-Schicht (nicht klassisch!):

┌─────────────────────────────────────────┐
│         Service Layer (Repository)       │  ← Öffentliche API (Service Contracts)
│   PostRepositoryInterface::getById()    │
├─────────────────────────────────────────┤
│              Model / Entity             │  ← Datenhaltung + Data Interface
│          Post extends AbstractModel     │
├─────────────────────────────────────────┤
│         Resource Model (DB-Zugriff)     │  ← SQL, Tabellenname, Primary Key
│    ResourceModel\Post extends AbstractDb│
├─────────────────────────────────────────┤
│         Collection (Mehrfach-Laden)     │  ← Iterator über mehrere Entities
│    ResourceModel\Post\Collection        │
└─────────────────────────────────────────┘

In klassischem MVC ist das Model oft eine einzige Klasse. In Magento sind es mindestens vier. Der Grund: Trennung von Datenpersistenz (Resource Model), Datenhaltung (Model/Entity), API-Kontrakt (Repository Interface) und Bulk-Operationen (Collection). Das erhöht die Testbarkeit und Flexibilität erheblich.


<?php
// In Magento: Kein direkter DB-Aufruf im Controller
// Der Controller kennt das Model nicht direkt

// FALSCH (klassisches MVC-Denken):
public function execute(): ResultInterface
{
    $product = $this->productModel->load($id); // Anti-Pattern in Magento
    // ...
}

// RICHTIG (Magento Service Contracts):
public function execute(): ResultInterface
{
    // Controller injiziert Repository (Service Layer), nicht Model direkt
    $product = $this->productRepository->getById($id);
    // ...
}

8. ViewModel: Von MVC zu MVVM im Hyvä Theme

Mit dem ViewModel-Pattern (verfügbar seit Magento 2.2, Standard im Hyvä Theme) nähert sich Magentos Architektur noch stärker dem MVVM-Muster (Model-View-ViewModel). Der ViewModel übernimmt die Präsentationslogik aus dem Block – und macht die View-Schicht testbar.


Magento-Architektur mit ViewModel (Hyvä):

HTTP Request
    ↓
FrontController → Router → Action/Controller
    ↓
ResultPage → Layout-XML
    ↓
Block (Rendering-Container)
    │
    ├── ViewModel (Präsentationslogik, testbar, kein Magento-Erbe)
    │       └── injiziert via Layout-XML
    │
    └── phtml-Template (HTML-Ausgabe)
            ↓
        PHP-Daten → JSON → Alpine.js (Interaktivität)

<?php
declare(strict_types=1);

namespace Mironsoft\Blog\ViewModel;

use Magento\Framework\View\Element\Block\ArgumentInterface;
use Mironsoft\Blog\Api\PostRepositoryInterface;
use Mironsoft\Blog\Api\Data\PostInterface;

/**
 * ViewModel: Provides presentation data for the blog post template.
 * No Magento base class inheritance — easily unit-testable.
 */
class PostViewModel implements ArgumentInterface
{
    public function __construct(
        private readonly PostRepositoryInterface $postRepository
    ) {}

    /**
     * Returns the current post or null if not found.
     */
    public function getPost(int $postId): ?PostInterface
    {
        try {
            return $this->postRepository->getById($postId);
        } catch (\Exception) {
            return null;
        }
    }

    /**
     * Returns formatted publication date.
     */
    public function formatPublishedAt(string $date): string
    {
        return (new \DateTimeImmutable($date))->format('d. F Y');
    }

    /**
     * Returns reading time in minutes based on word count.
     */
    public function getReadingTime(string $content): int
    {
        $wordCount = str_word_count(strip_tags($content));
        return max(1, (int) ceil($wordCount / 200));
    }
}

Das Ergebnis: Der Block ist nur noch ein dünner Rendering-Container. Alle Logik liegt im testbaren ViewModel. Das Template erhält Daten sauber getrennt. Das ist kein klassisches MVC mehr – das ist MVVM mit einer Layout-Orchestrierungsschicht darunter.

9. Vollständiger Beispiel-Flow: Blog-Post-Seite

Zusammenfassung am konkreten Beispiel – ein Benutzer öffnet /blog/post/view/id/42:


1. pub/index.php
   Bootstrap: Autoloading, DI-Container, ObjectManager

2. FrontController::dispatch()
   Router-Liste:
   → Standard Router matcht "mironsoft_blog" / "post" / "view"
   → Action-Klasse: Mironsoft\Blog\Controller\Post\View

3. Mironsoft\Blog\Controller\Post\View::execute()
   - Liest ?id=42 aus Request
   - Prüft ob Post existiert (Repository::getById(42))
   - Wirft NoSuchEntityException → Redirect zu 404
   - Oder: Gibt ResultPage zurück (kein HTML!)

4. ResultPage::renderResult()
   - Initialisiert das Layout-System
   - Aktive Layout-Handles:
     • "default" (alle Seiten)
     • "mironsoft_blog_post_view" (diese spezifische Seite)

5. Layout-XML-Merging
   - Lädt alle layout/mironsoft_blog_post_view.xml aller Module
   - Mergt mit default.xml, 1column.xml
   - Ergebnis: vollständiger Block-Baum in XML

6. Block-Instanziierung
   - DI-Container erstellt alle Block-Objekte
   - Injiziert ViewModel als Argument
   - Block-Baum: page > head, body > header, main, footer > blog.post.view

7. blog.post.view::toHtml()
   → Template: Mironsoft_Blog::post/view.phtml
   → $viewModel = $block->getData('view_model')
   → $post = $viewModel->getPost(42)   ← Datenbankzugriff hier!
   → HTML generieren und zurückgeben

8. HTTP Response
   → Browser rendert HTML

Dieser Flow macht deutlich: In klassischem MVC gibt der Controller die Daten an die View. In Magento holt sich die View (Block/ViewModel) die Daten selbst. Der Controller definiert nur den Seitentyp (ResultPage), das Layout-XML definiert die Struktur, und der Block/ViewModel holt die Daten.

Mironsoft

Magento 2 Architektur & Entwicklung

Magento-Architektur für dein Projekt?

Wir entwickeln Magento 2 Module und Themes mit sauberer Architektur: Service Contracts, ViewModels, Layout-XML und vollständige PHPUnit-Tests – nach aktuellen Best Practices.

Architektur-Review
Bestehende Magento-Module auf Architektur-Probleme prüfen: Anti-Patterns, ObjectManager-Aufrufe, fehlende Service Contracts.
Modul-Entwicklung
Neue Magento 2 Module mit sauberem MVC/MVVM-Ansatz: Controller, Layout-XML, ViewModel, Service Contracts.
Hyvä Migration
Luma-Templates auf Hyvä Theme migrieren: Block-Logik in ViewModels, Alpine.js für Interaktivität, Tailwind CSS.

10. Zusammenfassung

Magento 2 basiert auf MVC – aber erweitert es fundamental. Das klassische Dreieck aus Model, View und Controller ist in Magento um eine Layout-Orchestrierungsschicht (XML), ein Block-System und das ViewModel-Pattern ergänzt. Das Ergebnis ist keine einfache Architektur, aber eine für Enterprise-E-Commerce außerordentlich flexible.

MVC in Magento 2 – Unterschiede auf einen Blick

Controller

Gibt ResultPage zurück, kein HTML. Kein direktes Template-Rendering. Entscheidet Seitentyp, nicht Inhalt. Inhalt kommt aus Layout-XML.

Layout-XML (4. Schicht)

Definiert welche Blöcke auf welcher Seite erscheinen. Mehrere Module können dieselbe Seite erweitern. Kein klassisches MVC-Konzept.

Block + Template

Block = Rendering-Container (Magento-Basisklasse). Template = phtml-Datei (HTML + PHP). View holt Daten selbst via Block/ViewModel – nicht vom Controller.

Model (Service Contracts)

Nicht eine Klasse, sondern 4 Schichten: Repository Interface → Repository Impl. → Model → Resource Model. Service Contracts als stabile API nach außen.

11. FAQ: MVC in Magento 2

1 Größter Unterschied: Magento vs. klassisches MVC?
In klassischem MVC gibt der Controller Daten an die View. In Magento gibt der Controller keine Daten — er gibt ein ResultPage-Objekt zurück. Was auf der Seite erscheint, bestimmt das Layout-XML. Die View-Schicht (Block/ViewModel) holt Daten selbst aus Repositories.
2 Was macht der FrontController in Magento 2?
Kein klassischer Controller — ein Dispatcher. Er durchsucht eine Liste von Routern (Standard, CMS, URL-Rewrite...) um die passende Action-Klasse zu finden. Entspricht dem GoF Front Controller Pattern — ein einziger Einstiegspunkt für alle Requests.
3 Was ist ein Layout-Handle in Magento 2?
Eine ID, die bestimmt welche Layout-XML-Dateien geladen werden. Jede Seite hat mehrere aktive Handles: default, catalog_product_view, catalog_product_view_type_simple. Alle passenden XMLs werden gemergt → finale Block-Struktur.
4 Warum gibt der Controller kein Template zurück?
Absichtlich: Trennung von „Welcher Seitentyp" (Controller) und „Welcher Inhalt" (Layout-XML). Mehrere Module können dieselbe Seite erweitern ohne den Controller zu ändern — via eigene layout/handle.xml-Dateien. Das ist in klassischem MVC nicht vorgesehen.
5 Was ist der Unterschied zwischen Block und Template?
Block: PHP-Klasse, Rendering-Container im Layout-System, verwaltet Caching und Child-Blocks. Template: phtml-Datei, rendert das eigentliche HTML. Immer als Paar: Block stellt Logik/ViewModel, Template rendert HTML. Moderne Empfehlung: Logik in ViewModels, nicht in Block-Klassen.
6 Wie verarbeitet Magento einen HTTP-Request?
1. pub/index.php → Bootstrap. 2. FrontController → Router-Suche. 3. Action-Klasse gefunden. 4. execute() → ResultPage. 5. Layout-XML laden & mergen. 6. Block-Baum aufbauen. 7. toHtml() rekursiv → HTML. 8. HTTP Response.
7 Was ist der Unterschied zwischen Area und Store-Scope?
Area: technische Trennlinie (frontend, adminhtml, crontab). Eigene di.xml, Layout-XML, Observer je Area. Store-Scope: Business-Konfigurationsebene (Website → Store → Store View). Unabhängig voneinander.
8 Ist Magento 2 eher MVC oder MVVM?
Mit ViewModel-Pattern (besonders in Hyvä) näher an MVVM: Block = View, ViewModel = ViewModel, Repository = Model. Aber die Layout-XML-Orchestrierungsschicht macht es zu einem Magento-spezifischen Hybrid — manchmal als HMVC (Hierarchical MVC) bezeichnet.
9 Warum holt sich die View ihre Daten selbst?
Maximale Entkopplung: Der Mini-Cart-Block im Header erscheint auf jeder Seite — der Controller einer Produktseite weiß nichts davon und muss es nicht. Jeder Block ist für seine eigenen Daten verantwortlich. Separation of Concerns auf Block-Ebene statt Controller-Ebene.
10 Wie verändert Hyvä Theme Magentos Architektur?
Hyvä ersetzt nur die View-Schicht: kein Knockout.js, kein jQuery, keine UI-Components. Stattdessen Alpine.js + Tailwind CSS. ViewModels werden zur Standard-Methode. Controller- und Layout-XML-Schicht bleiben unverändert. Ergebnis: deutlich einfachere, schnellere und besser testbare Frontend-Schicht.