und warum ViewModels heute die bessere dritte Option sind
Die Entscheidung Block vs. Template wird in den meisten Magento-2-Projekten intuitiv getroffen, selten bewusst begründet. Die Folge sind fette Block-Klassen mit Business-Logik, Templates mit direkten Model-Aufrufen und Code, der sich nicht isoliert testen lässt. Dieser Artikel zeigt, was wirklich in eine Block-Klasse gehört, was ins phtml-Template und wie ViewModels die Trennung in modernen Magento-2.4.8- und Hyva-Projekten sauber auflösen.
Inhaltsverzeichnis
- 1. Block vs. Template: die eigentliche Entscheidung
- 2. Was gehört in eine Block-Klasse
- 3. Was gehört ins Template, und was nicht
- 4. ViewModels: die dritte Option jenseits von Block und Template
- 5. Anti-Patterns aus echten Magento-Projekten
- 6. Block-Caching: getCacheKeyInfo, _toHtml und cacheable=false
- 7. getChildBlock und getChildHtml: wann Block-Komposition noch nötig ist
- 8. Praxisbeispiel: vom fetten Block zum ViewModel
- 9. Block vs. Template vs. ViewModel im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Block vs. Template: die eigentliche Entscheidung
Jede Magento-2-Ausgabe entsteht aus einem Zusammenspiel von zwei Bausteinen: einer Block-Klasse, die von \Magento\Framework\View\Element\Template erbt, und einem Template, einer phtml-Datei, die von genau diesem Block gerendert wird. Die Frage Block vs. Template klingt banal, ist in der Praxis aber die Wurzel vieler Architekturprobleme. Die verbreitete Faustregel "Block enthält Logik, Template zeigt nur an" stimmt im Kern, lässt aber offen, welche Art von Logik in die Block-Klasse gehört und welche stattdessen in einen Service Contract oder ein ViewModel ausgelagert werden sollte.
In vielen gewachsenen Magento-Projekten wird diese Entscheidung nie bewusst getroffen. Entwickler kopieren ein bestehendes Block-Template-Paar, fügen eine Methode hinzu, wo gerade Platz ist, und schon wandert Business-Logik dorthin, wo sie architektonisch nicht hingehört. Nach ein paar Jahren stehen Block-Klassen mit zwanzig Methoden, direkten ResourceModel-Aufrufen und Preisberechnungen neben Templates, die parallel eigene Datenbankabfragen absetzen. Block vs. Template ist damit keine akademische Frage, sondern entscheidet direkt über Testbarkeit, Wiederverwendbarkeit und Wartungsaufwand eines Moduls.
Dieser Artikel ordnet die drei relevanten Bausteine, Block-Klasse, Template und ViewModel, entlang ihrer eigentlichen Verantwortung ein. Er zeigt konkrete Anti-Patterns aus realen Magento-Projekten, erklärt die Rolle von Block-Caching und Child-Block-Komposition und endet mit einem vollständigen Refactoring-Beispiel, das eine fette Block-Klasse in ein testbares ViewModel mit Service-Contract-Abhängigkeit überführt.
2. Was gehört in eine Block-Klasse
Eine Block-Klasse ist im Kern ein Adapter zwischen Layout-System und Template. Ihre legitime Verantwortung umfasst drei Dinge: Aufbau der Kindblock-Struktur in _prepareLayout(), Steuerung des Caching-Verhaltens über getCacheKeyInfo() und gegebenenfalls _toHtml(), sowie das Bereitstellen von bereits aufbereiteten Daten für das Template über öffentliche Getter-Methoden. Wichtig dabei: "bereitstellen" heißt nicht "berechnen". Eine Block-Klasse darf einen Service Contract oder ein Repository aufrufen, um Daten zu holen, sollte die eigentliche Geschäftslogik aber nicht selbst implementieren.
Ein sauberes Beispiel: Ein Block, der die zuletzt angesehenen Produkte anzeigt, darf im Konstruktor eine ProductRepositoryInterface-Instanz injizieren und in einer Methode getRecentProducts(): array die Repository-Aufrufe kapseln. Was nicht in die Block-Klasse gehört, ist die Berechnung von Rabattstaffeln, die Formatierung komplexer Preislogik oder das Zusammenführen mehrerer Datenquellen mit eigenen Bedingungen. Genau an dieser Stelle kippt eine Block-Klasse von "View-Adapter" zu "verstecktem Service", und genau das macht Unit-Tests schwierig, weil \Magento\Framework\View\Element\Template tief mit dem Layout-System, dem Request-Objekt und dem Rendering-Kontext verkoppelt ist.
Für Block vs. Template gilt deshalb als erste harte Regel: Eine Block-Klasse bereitet Daten vor und reicht sie durch, sie erfindet keine neue Business-Logik. Sobald eine Methode in der Block-Klasse mehr als reines Delegieren an einen Service Contract macht, etwa Schleifen mit Bedingungen, Aggregationen oder Formatierungsregeln enthält, ist das ein Signal, diese Logik in ein ViewModel oder direkt in einen Service Contract zu verschieben.
3. Was gehört ins Template, und was nicht
Ein Template in Magento 2 ist eine phtml-Datei, die ausschließlich für Präsentation zuständig ist. Erlaubt sind: Iteration über bereits aufbereitete Daten, bedingtes Rendern von Markup abhängig von einfachen Flags, das Setzen von Alpine.js-Datenattributen für Hyva-Komponenten und das escapen jeder Ausgabe über $block->escapeHtml(), escapeHtmlAttr() oder escapeUrl(). Nicht erlaubt sind Aufrufe an Repositories, ResourceModels oder den ObjectManager, eigene Preisberechnungen, Datumsformatierung mit komplexer Logik oder Bedingungen, die eigentlich Geschäftsregeln abbilden.
In der Praxis ist die Grenze zwischen Template vs. Block-Logik oft fließend, weil phtml-Dateien technisch jeden PHP-Code ausführen können. Genau das ist das Problem: Ein Template, das ObjectManager::getInstance()->get(SomeModel::class) aufruft, umgeht die komplette Dependency-Injection-Architektur, ist nicht testbar und bricht bei jedem Refactoring der zugrunde liegenden Klasse unbemerkt. Ein Template sollte niemals mehr wissen müssen, als der zugehörige Block oder das ViewModel ihm über die öffentliche Schnittstelle mitteilt.
Für Hyva-Themes kommt eine weitere Dimension hinzu: Alpine.js-Komponenten binden sich über x-data an Werte, die aus PHP nach JSON serialisiert werden. Diese Serialisierung gehört ins Template, die Aufbereitung der zugrunde liegenden Daten aber ins ViewModel oder in die Block-Klasse. Ein Template, das json_encode() auf eine rohe Collection anwendet, statt auf ein bereits normalisiertes Array, koppelt die Präsentationsschicht an interne Datenstrukturen, die sich jederzeit ändern können.
4. ViewModels: die dritte Option jenseits von Block und Template
Seit Magento 2.2 existiert mit \Magento\Framework\View\Element\Block\ArgumentInterface ein leeres Marker-Interface, das die Grundlage für ViewModels bildet. Ein ViewModel ist eine gewöhnliche PHP-Klasse ohne jede Kopplung an das View-Layer, an Block, Request oder Layout. Sie wird über Layout-XML als Argument an einen beliebigen Block gebunden und ist damit über mehrere Blocks und Templates hinweg wiederverwendbar, ohne dass diese Blocks eine gemeinsame Basisklasse teilen müssen. Genau das löst das strukturelle Problem von Block vs. Template auf einer neuen Ebene: Business-Logik wandert weder in die Block-Klasse noch ins Template, sondern in eine eigenständige, unit-testbare Klasse.
Die Vorteile gegenüber einer fetten Block-Klasse sind konkret. Erstens Testbarkeit: Ein ViewModel lässt sich ohne Magento-Bootstrap instanziieren und mit gemockten Service Contracts testen, während ein Block-Test in der Regel den kompletten Layout-Kontext benötigt. Zweitens Entkopplung: Ein ViewModel kennt weder Request noch Response noch Layout, es kennt nur die Service Contracts, die es im Konstruktor erhält. Drittens Wiederverwendbarkeit: Dasselbe ViewModel kann an mehrere Blocks in unterschiedlichen Layout-Handles gebunden werden, ohne Vererbungshierarchien aufzubauen, die in Magento historisch zu tief verschachtelten Block-Klassen geführt haben.
In der Layout-XML wird ein ViewModel über <argument name="view_model" xsi:type="object"> an einen Block gebunden. Im Template greift man über $block->getViewModel() darauf zu, sofern die Block-Klasse eine typisierte Getter-Methode dafür bereitstellt. Diese kleine Konvention ist wichtig: Statt $block->getData('view_model') im Template direkt zu verwenden, was keinerlei Typsicherheit bietet, sollte die Block-Klasse eine explizite Methode getViewModel(): ProductBadgeViewModel anbieten. So bleibt Block vs. Template vs. ViewModel eine saubere, typsichere Kette statt einer impliziten Datenweitergabe über generische Arrays.
<!-- File: view/frontend/layout/catalog_product_view.xml -->
<!-- Wiring a ViewModel as an argument to a block, no coupling between blocks required -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="content">
<block class="Magento\Framework\View\Element\Template"
name="mironsoft.product.badge"
template="Mironsoft_Catalog::product/badge.phtml">
<arguments>
<argument name="view_model" xsi:type="object">
Mironsoft\Catalog\ViewModel\ProductBadge
</argument>
</arguments>
</block>
</referenceContainer>
</body>
</page>
<?php
declare(strict_types=1);
namespace Mironsoft\Catalog\ViewModel;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\View\Element\Block\ArgumentInterface;
/**
* View model providing prepared product badge data for the storefront.
* No View-layer coupling, fully unit testable with a mocked repository.
*/
final class ProductBadge implements ArgumentInterface
{
/**
* @param ProductRepositoryInterface $productRepository Service contract for product access
*/
public function __construct(
private readonly ProductRepositoryInterface $productRepository
) {
}
/**
* Returns the badge label for a given product SKU, or null if none applies.
*
* @param string $sku Product SKU to check
* @return string|null Prepared, already translated badge label
*/
public function getBadgeLabel(string $sku): ?string
{
try {
$product = $this->productRepository->get($sku);
} catch (NoSuchEntityException) {
return null;
}
return $this->resolveBadge($product);
}
/**
* Resolves the badge text based on stock status and special price.
*
* @param ProductInterface $product Loaded product entity
* @return string|null Badge text or null if no badge applies
*/
private function resolveBadge(ProductInterface $product): ?string
{
// @phpstan-ignore-next-line getSpecialPrice not in ProductInterface but present on the model
if ($product->getSpecialPrice()) {
return 'Sale';
}
return null;
}
}
5. Anti-Patterns aus echten Magento-Projekten
Das häufigste Anti-Pattern bei Block vs. Template ist Business-Logik im phtml-Template. Typisches Beispiel: Ein Template berechnet innerhalb einer Schleife den effektiven Preis inklusive Steuer und Rabatt, statt diese Berechnung von einem Service Contract oder einer ViewModel-Methode zu beziehen. Sobald sich die Steuerlogik ändert, muss der Entwickler jedes Template durchsuchen, das diese Berechnung dupliziert hat, statt eine einzige Klasse anzupassen.
Ein zweites, ebenso verbreitetes Anti-Pattern sind direkte Model- oder ResourceModel-Aufrufe aus dem Template heraus, oft über den ObjectManager. Das umgeht Dependency Injection vollständig, verhindert, dass PHPStan oder statische Analyse die Abhängigkeit erkennt, und macht das Template abhängig von internen Implementierungsdetails, die sich bei einem Magento-Upgrade ändern können. Genau dieses Muster war in Luma-Themes verbreitet und findet sich in migriertem Legacy-Code oft unverändert in Hyva-Templates wieder.
Das dritte Anti-Pattern betrifft fehlendes Escaping: echo $someVariable statt echo $block->escapeHtml($someVariable). Sobald der Wert aus einer Kundeneingabe, einem CMS-Block oder einem Attributwert stammt, öffnet das ungeschütztes HTML-Echo eine potenzielle Cross-Site-Scripting-Lücke. Magento erzwingt Escaping nicht automatisch in phtml-Dateien, weshalb jede Ausgabe, die nicht nachweislich aus einer statischen, vertrauenswürdigen Quelle stammt, über escapeHtml(), escapeHtmlAttr() oder escapeJs() laufen muss. In Hyva-Projekten kommt escapeUrl() für Links und der Alpine.js-Datenserialisierung besondere Bedeutung zu, weil dort JSON direkt in HTML-Attribute eingebettet wird.
6. Block-Caching: getCacheKeyInfo, _toHtml und cacheable=false
Block-Klassen in Magento 2 können ihre gerenderte Ausgabe cachen, gesteuert über die Methode getCacheKeyInfo(), die ein Array von Schlüsselsegmenten zurückgibt, typischerweise Blockklasse, Template, Store-ID und Kundengruppe. Magento kombiniert diese Segmente zu einem eindeutigen Cache-Key und speichert das gerenderte HTML im konfigurierten Cache-Backend. Wird _toHtml() überschrieben, sollte die Elternimplementierung weiterhin aufgerufen werden, es sei denn, es gibt einen expliziten Grund, das Standard-Caching zu umgehen, etwa bei Blocks, deren Inhalt sich pro Request ändert.
In Hyva-Projekten stellt sich die Caching-Frage anders als in Luma. Weil Hyva bewusst auf serverseitiges Block-HTML-Caching für personalisierte Bereiche verzichtet und stattdessen auf volle Seiten-Caches mit clientseitigem Nachladen setzt, wird für dynamische, kundenspezifische Blocks häufig cacheable="false" in der Layout-XML gesetzt. Das betrifft typischerweise den Mini-Cart, Begrüßungstexte mit Kundennamen oder Wishlist-Zähler. Diese Blocks werden entweder komplett von der Full-Page-Cache-Speicherung ausgenommen oder ihre dynamischen Anteile werden per Alpine.js aus einem separaten, nicht gecachten Endpunkt nachgeladen, während der statische Rest der Seite normal gecacht bleibt.
Ein wichtiger CSP-Aspekt: Jeder Inline-<script>-Block in einem Hyva-Template muss über $hyvaCsp->registerInlineScript() registriert werden, sonst blockiert die Content-Security-Policy die Ausführung. Das gehört zwingend ins Template, nicht in die Block-Klasse, weil die Registrierung an den konkreten Rendering-Zeitpunkt gebunden ist. Ein Block, der versucht, CSP-Registrierung im Konstruktor vorwegzunehmen, funktioniert nicht zuverlässig, weil der CSP-Nonce erst zur Rendering-Zeit final feststeht.
7. getChildBlock und getChildHtml: wann Block-Komposition noch nötig ist
Nicht jede Struktur lässt sich in ein einzelnes ViewModel auflösen. Wenn eine Seite aus mehreren unabhängigen, in der Layout-XML konfigurierbaren Bereichen besteht, etwa die Produktdetailseite mit Bewertungen, verwandten Produkten und einem Tab-System, bleibt die klassische Block-Komposition über $block->getChildBlock('name') und $block->getChildHtml('name') das richtige Werkzeug. Jeder Kindblock bringt sein eigenes Template mit, kann unabhängig cachebar sein und lässt sich über Layout-XML durch Module von Drittanbietern erweitern oder ersetzen, ohne den Elternblock anzufassen.
Der Unterschied zu ViewModels liegt im Zweck: getChildHtml() löst ein strukturelles Kompositionsproblem, mehrere unabhängig renderbare Bereiche zu einer Seite zusammenzufügen, während ein ViewModel ein Datenaufbereitungsproblem innerhalb eines einzelnen Templates löst. Beide Mechanismen schließen sich nicht aus, sie ergänzen sich: Ein Elternblock kann mehrere Kindblöcke komponieren, während jeder einzelne Kindblock sein eigenes ViewModel für die Datenaufbereitung nutzt. Wer versucht, Block-Komposition durch ViewModels zu ersetzen, verliert die Erweiterbarkeit über Layout-XML, die gerade in Drittanbieter-Integrationen unverzichtbar ist.
Ein häufiger Fehler in diesem Zusammenhang: Ein Elternblock greift über getChildBlock('name') direkt auf interne Getter-Methoden des Kindblocks zu, um dessen Daten weiterzuverarbeiten. Das koppelt beide Blocks so eng, dass keiner von beiden mehr unabhängig geändert werden kann. Wenn Daten zwischen Elternblock und Kindblock geteilt werden müssen, ist ein gemeinsames ViewModel, das beiden als Argument gebunden wird, fast immer die sauberere Lösung als eine direkte Objektreferenz zwischen zwei Blocks.
8. Praxisbeispiel: vom fetten Block zum ViewModel
Das folgende Beispiel zeigt eine typische fette Block-Klasse, wie sie in gewachsenen Magento-Projekten häufig anzutreffen ist. Die Klasse lädt direkt eine Produkt-Collection über das ResourceModel, berechnet Preise inline und die Template-Ausgabe verzichtet an einer Stelle auf Escaping. Dieses Muster ist schwer zu testen, weil ein Unit-Test den kompletten Objektgraphen von Collection, ResourceModel und Preis-Helper mocken müsste, und es verletzt die Regel, dass eine Block-Klasse Daten durchreicht statt Business-Logik zu implementieren.
<?php
declare(strict_types=1);
namespace Mironsoft\Catalog\Block;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Framework\View\Element\Template;
use Magento\Framework\View\Element\Template\Context;
/**
* BEFORE: fat block class mixing data access, business logic and rendering concerns.
* Anti-pattern: direct ResourceModel access, price math inline, hard to unit test.
*/
class RecentProducts extends Template
{
public function __construct(
Context $context,
private readonly CollectionFactory $collectionFactory,
array $data = []
) {
parent::__construct($context, $data);
}
/**
* Loads recent products and computes discounted prices inline.
* Anti-pattern: business logic embedded directly in the block class.
*
* @return array
*/
public function getRecentProducts(): array
{
$collection = $this->collectionFactory->create();
$collection->addAttributeToSelect(['name', 'price', 'special_price']);
$collection->setPageSize(5)->setCurPage(1);
$result = [];
foreach ($collection as $product) {
// Anti-pattern: tax and discount math should live in a price service, not here
$price = (float) $product->getPrice();
$special = (float) $product->getSpecialPrice();
$finalPrice = $special > 0 && $special < $price ? $special : $price;
$result[] = [
'name' => $product->getName(),
'price' => $finalPrice,
'url' => $product->getProductUrl(),
];
}
return $result;
}
}
Die Auflösung trennt drei Zuständigkeiten sauber voneinander: Die Preisberechnung wandert in ein PricingHelper-artiges Service-Contract-Konstrukt, die Datenaufbereitung in ein ViewModel, und die Block-Klasse wird auf eine dünne Delegationsschicht reduziert, die nur noch das ViewModel typsicher durchreicht. Das Template selbst greift ausschließlich über $block->getViewModel() auf bereits fertig aufbereitete Daten zu und escaped jede Ausgabe konsequent.
<?php
declare(strict_types=1);
namespace Mironsoft\Catalog\ViewModel;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Framework\Pricing\Helper\Data as PricingHelper;
use Magento\Framework\View\Element\Block\ArgumentInterface;
/**
* AFTER: thin view model, no View-layer coupling, fully unit testable.
* Business logic (price resolution) is delegated to Magento's own pricing helper.
*/
final class RecentProducts implements ArgumentInterface
{
/**
* @param CollectionFactory $collectionFactory Product collection factory
* @param PricingHelper $pricingHelper Service contract for price formatting
*/
public function __construct(
private readonly CollectionFactory $collectionFactory,
private readonly PricingHelper $pricingHelper
) {
}
/**
* Returns the five most recently added products as a plain, template-ready array.
*
* @return array<int, array{name: string, price: string, url: string}>
*/
public function getRecentProducts(): array
{
$collection = $this->collectionFactory->create();
$collection->addAttributeToSelect(['name', 'price', 'special_price']);
$collection->setPageSize(5)->setCurPage(1);
$result = [];
foreach ($collection as $product) {
/** @var ProductInterface $product */
$result[] = [
'name' => $product->getName(),
'price' => $this->pricingHelper->currency($product->getFinalPrice(), true, false),
'url' => $product->getProductUrl(),
];
}
return $result;
}
}
<?php
/** @var \Magento\Framework\View\Element\Template $block */
/** @var \Magento\Framework\Escaper $escaper */
/** @var \Mironsoft\Catalog\ViewModel\RecentProducts $viewModel */
$viewModel = $block->getViewModel();
?>
<div class="recent-products" x-data="{ open: false }">
<button type="button" @click="open = !open" class="text-sm font-semibold">
<?= $escaper->escapeHtml(__('Recently Viewed')) ?>
</button>
<ul x-show="open" class="mt-2 space-y-1">
<?php foreach ($viewModel->getRecentProducts() as $product): ?>
<li>
<a href="<?= $escaper->escapeUrl($product['url']) ?>" class="text-sm">
<?= $escaper->escapeHtml($product['name']) ?>
<span class="text-gray-500"><?= $escaper->escapeHtml($product['price']) ?></span>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>
9. Block vs. Template vs. ViewModel im Vergleich
Die folgende Tabelle fasst zusammen, welche Verantwortung wohin gehört. Sie ist als schnelle Entscheidungshilfe gedacht, wenn beim Schreiben eines neuen Moduls die Frage Block vs. Template vs. ViewModel wieder auftaucht.
| Verantwortung | Gehört in Block-Klasse | Gehört ins Template | Gehört ins ViewModel |
|---|---|---|---|
| Kindblock-Struktur | _prepareLayout(), getChildHtml() | Nein, keine Struktur-Verwaltung | Nicht zuständig |
| Business-Logik / Preisregeln | Nein, nur delegieren | Nein, klassisches Anti-Pattern | Ja, mit Service-Contract-Abhängigkeit |
| Caching-Strategie | getCacheKeyInfo(), _toHtml() | Nein, keine Cache-Steuerung | Nicht zuständig |
| Iteration & Markup | Nein, das ist Präsentation | Ja, über bereits fertige Daten | Nicht zuständig |
| Escaping der Ausgabe | escapeHtml() bereitstellen | Ja, bei jeder Ausgabe zwingend | Nicht zuständig |
| Wiederverwendung über Blocks hinweg | Nein, an View-Layer gekoppelt | Nein, an ein Template gebunden | Ja, per Layout-XML mehrfach bindbar |
| Unit-Testbarkeit ohne Bootstrap | Schwierig, Layout-Kontext nötig | Praktisch nicht testbar | Ja, reines PHP ohne View-Kopplung |
Die Tabelle macht deutlich, dass Block vs. Template kein binäres Entweder-Oder ist, sondern durch das ViewModel zu einer Dreiteilung wird. Die Block-Klasse bleibt für strukturelle und Caching-Aspekte zuständig, das Template für reine Präsentation, und das ViewModel übernimmt die Datenaufbereitung mit Business-Logik. Wer diese Dreiteilung konsequent einhält, reduziert sowohl die Größe einzelner Klassen als auch die Kopplung zwischen ihnen.
10. Zusammenfassung
Block vs. Template ist am Ende keine Frage von Geschmack, sondern von Verantwortungsgrenzen. Eine Block-Klasse steuert Kindblock-Struktur und Caching und stellt bereits aufbereitete Daten bereit, ein Template rendert diese Daten ohne eigene Business-Logik und escaped konsequent jede Ausgabe. Sobald Datenaufbereitung mit echter Geschäftslogik ins Spiel kommt, gehört sie in ein ViewModel, das über ArgumentInterface deklariert und per Layout-XML gebunden wird, ohne jede Kopplung an das View-Layer.
Der praktische Nutzen zeigt sich vor allem in Testbarkeit und Wartbarkeit: Ein ViewModel lässt sich ohne Magento-Bootstrap unit-testen, mehrfach über verschiedene Blocks hinweg wiederverwenden und unabhängig von Layout-Details weiterentwickeln. Wer bei jeder neuen Anforderung kurz die Frage Block, Template oder ViewModel stellt, verhindert von Anfang an, dass sich Business-Logik unbemerkt in phtml-Dateien oder aufgeblähten Block-Klassen ansammelt.
Block vs. Template: Das Wichtigste auf einen Blick
Block-Klasse
Steuert Kindblock-Struktur, Caching via getCacheKeyInfo() und reicht bereits aufbereitete Daten durch, implementiert aber keine Business-Logik selbst.
Template
Reine Präsentation: Iteration, bedingtes Markup, Alpine.js-Datenattribute, konsequentes escapeHtml() bei jeder Ausgabe.
ViewModel
Implementiert ArgumentInterface, kapselt Service-Contract-Aufrufe und Business-Logik, ist unit-testbar ohne View-Layer-Kopplung.
Anti-Patterns vermeiden
Kein ObjectManager im Template, keine Preislogik im phtml, kein Echo ohne escapeHtml(), keine direkte Model-Kopplung zwischen Blocks.
11. FAQ: Block vs. Template vs. ViewModel
1Was ist der grundsätzliche Unterschied zwischen Block vs. Template?
2Wann statt Block-Klasse ein ViewModel verwenden?
3Darf ein Template einen Repository-Aufruf machen?
4Wie wird ein ViewModel an einen Block gebunden?
5Warum ist ArgumentInterface ein leeres Interface?
6Wann brauche ich noch getChildBlock und getChildHtml?
7Wie funktioniert Block-Caching mit getCacheKeyInfo?
8Warum cacheable=false in Hyva-Themes?
9Häufigstes Sicherheitsproblem bei Templates?
10Lohnt sich Refactoring von Block zu ViewModel?
Mironsoft
Magento-2-Architektur, Hyva-Themes und ViewModel-Refactoring
Bereit für saubere Magento-2-Architektur?
Vom Architektur-Review bis zum vollständigen ViewModel-Refactoring: Wir bringen Block, Template und ViewModel in eurem Projekt wieder in die richtige Reihenfolge.
Code-Audit
Systematische Prüfung aller Block-Klassen und Templates auf Anti-Patterns
Refactoring
Migration zu ViewModels mit Service Contracts und vollständiger Testabdeckung
Team-Schulung
Best Practices für Block vs. Template vs. ViewModel im eigenen Entwicklerteam verankern