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 volleTemplate\Contextmuss mitgebaut werden. - Wiederverwendbarkeit: ViewModel - derselbe ViewModel lässt sich in mehreren Blocks/Layout-Handles einhängen (Kapitel 52 nutzt
PointsBalanceein 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.xmlinstanziiert 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.