Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Block vs. ViewModel: wann welches genutzt wird

Block vs. ViewModel: wann welches genutzt wird

~6 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026

CLAUDE.md ist eindeutig: "Bevorzuge ViewModels (ArgumentInterface) gegenüber Block-Klassen." Bisher war diese Regel abstrakt - kein Kapitel brauchte eine Template-Datenquelle. Mit der Historie-Seite (Kapitel 45) und dem Prämienkatalog (Kapitel 49) ändert sich das: Beide Templates brauchen Daten, die irgendwo herkommen müssen. Dieses Kapitel zeigt beide Ansätze konkret gegenübergestellt, bevor Kapitel 48 den tatsächlich verwendeten ViewModel baut.

Der klassische Block-Ansatz (nicht das Ziel dieser Serie)

So sähe eine Punktestand-Anzeige nach klassischem Luma-Muster aus - reines Anschauungsmaterial, dieser Code wird in diesem Modul nirgends tatsächlich angelegt:

// NICHT Teil dieses Moduls - reines Gegenbeispiel
class PointsBalance extends \Magento\Framework\View\Element\Template
{
    public function __construct(
        \Magento\Framework\View\Element\Template\Context $context,
        private readonly \Magento\Customer\Model\Session $customerSession,
        private readonly \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository,
        array $data = []
    ) {
        parent::__construct($context, $data);
    }

    public function getPointsBalance(): int
    {
        // Geschäftslogik direkt im Block - der erste Schritt zum "God Object"
        $customer = $this->customerRepository->getById(
            (int) $this->customerSession->getCustomerId()
        );
        $attribute = $customer->getCustomAttribute('loyalty_points_balance');

        return $attribute !== null ? (int) $attribute->getValue() : 0;
    }
}

Achtung: Der offensichtliche Fehler ist hier nicht die Geschäftslogik selbst - getCustomAttribute() ist derselbe service-contract-konforme Zugriff, den auch der ViewModel-Ansatz nutzt. Der Fehler ist die Kopplung an \Magento\Framework\View\Element\Template: Ein Unit-Test müsste den kompletten Context mit all seinen eigenen Abhängigkeiten (URL-Builder, Layout, Event-Manager, ...) konstruieren oder mocken, nur um getPointsBalance() zu testen. Genau dieses Problem beschreibt Kapitel 91 (Unit Tests) ausführlich am Beispiel von PointsCalculator - Blocks sind in der Praxis kaum sinnvoll isoliert testbar.

Der ViewModel-Ansatz (ab Kapitel 48 in diesem Modul)

Ein ArgumentInterface-ViewModel hat keine Basisklasse, keinen Context, keine Kopplung an das View-Layer-Objektmodell - es ist ein gewöhnliches, per Constructor Property Promotion injiziertes PHP-Objekt:

// Vorschau auf Kapitel 48 - vollständige Klasse dort
class PointsBalance implements \Magento\Framework\View\Element\Block\ArgumentInterface
{
    public function __construct(
        private readonly \Magento\Customer\Model\Session $customerSession,
        private readonly \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository,
    ) {
    }

    public function getPointsBalance(): int { /* ... */ }
}

Im Layout-XML (vollständig in Kapitel 51) landet dafür - und das ist der Punkt, den viele Einsteiger übersehen - trotzdem ein <block>-Tag im XML. Der Unterschied: Die Block-Klasse ist die generische Core-Klasse Magento\Framework\View\Element\Template selbst, keine eigene Unterklasse. Der ViewModel wird ihr lediglich als view_model-Argument injiziert und im Template über $block->getViewModel() abgerufen:

<!-- Vorschau auf Kapitel 51 -->
<block class="Magento\Framework\View\Element\Template"
       name="loyalty.points.balance"
       template="Mironsoft_Loyalty::widget/points-balance.phtml">
    <arguments>
        <argument name="view_model" xsi:type="object">Mironsoft\Loyalty\ViewModel\PointsBalance</argument>
    </arguments>
</block>

Gegenüberstellung

  • Testbarkeit: ViewModel - new PointsBalance($sessionMock, $repositoryMock) reicht für einen Unit-Test. Block - der volle Template\Context muss mitgebaut werden.
  • Wiederverwendbarkeit: ViewModel - derselbe ViewModel lässt sich in mehreren Blocks/Layout-Handles einhängen (Kapitel 52 nutzt PointsBalance ein zweites Mal auf dem Dashboard). Block - eine Unterklasse ist an genau ein Template-Ökosystem gebunden.
  • Kopplung: ViewModel - kennt nur das, was es per Constructor injiziert bekommt. Block - erbt automatisch die komplette Template-API (URL-Building, Layout-Zugriff, Kind-Blocks), auch wenn nichts davon gebraucht wird.
  • Magento-Konvention seit 2.2: ViewModel ist der offiziell empfohlene Weg für reine Anzeigedaten. Block bleibt die technische Voraussetzung für alles, was Magento zur Laufzeit tatsächlich als AbstractBlock-Instanz im Layout-Baum erwartet.

Wann ein Block trotzdem noch nötig ist

Diese Serie widerspricht der ViewModel-Regel an keiner Stelle - drei spätere Blöcke zeigen aber ehrlich, wo Magentos eigene Kernkonventionen eine echte Block-Unterklasse erzwingen, weil das Framework an genau dieser Stelle eine feste Basisklasse erwartet:

  • Widget (Kapitel 55-57): widget.xml instanziiert direkt eine \Magento\Widget\Block\BlockInterface-Implementierung.
  • Page Builder Content Type (Kapitel 58-60): Das Preview-Template im Admin läuft über einen eigenen, von Page Builder vorgegebenen Block.
  • Payment-/Shipping-Method-Block (Kapitel 62-69): Die Checkout-Method-Renderer-Registrierung erwartet eine Block-Klasse, die die vom jeweiligen Core-Modul vorgegebene Basisklasse erweitert.

Tipp: Die Faustregel aus Kapitel 44 gilt unverändert: ViewModel ist der Standard für alles, was ein Hyvä-Storefront-Template an reinen Anzeigedaten braucht. Ein Block ist keine gleichwertige Alternative, die man "auch nehmen könnte" - er ist nur dort gerechtfertigt, wo Magento selbst eine AbstractBlock-Instanz technisch verlangt. Kapitel 48 baut jetzt den ersten echten ViewModel dieses Blocks.