Hyvä ViewModel-Pattern im Frontend: ViewModels sauber vom Block trennen
AI generated
Hyvä
phtml
Hyvä · Magento 2 · Tailwind CSS · Alpine.js
Hyvä ViewModel-Pattern im Frontend
Daten sauber vom Block trennen

Wer Business-Logik direkt in Block-Klassen schreibt, baut Magento-Templates, die sich weder isoliert testen noch sauber wiederverwenden lassen. Das Hyvä ViewModel-Pattern trennt Datenaufbereitung strikt von Darstellung, macht jede Regel als reine PHP-Klasse mit PHPUnit prüfbar und hält phtml-Templates auf reine Ausgabe beschränkt.

18 Min. Lesezeit ArgumentInterface · di.xml · Repository-Injection · PHPUnit Magento 2.4.8-p4 · PHP 8.4 · Hyvä Themes

1. Warum Hyvä auf ViewModels statt Block-Logik setzt

In klassischen Luma-Templates war es üblich, Formatierungslogik, Preisberechnungen und Sichtbarkeitsregeln direkt in die Block-Klasse zu schreiben, die von \Magento\Framework\View\Element\Template erbt. Das Ergebnis waren Block-Klassen mit dutzenden Methoden, die Geschäftslogik, Datenzugriff und Präsentationsdetails vermischten. Hyvä bricht bewusst mit diesem Muster: Statt neuer Methoden in der Block-Klasse verlangt Hyvä konsequent das Hyvä ViewModel-Pattern, bei dem jede fachliche Regel in einer eigenständigen, injizierbaren PHP-Klasse lebt.

Der Grund ist Single-Responsibility: Eine Block-Klasse soll die Verbindung zwischen Layout-XML, Kind-Blöcken und Template herstellen, nicht aber Geschäftsregeln implementieren. Ein Hyvä ViewModel übernimmt genau diese eine Aufgabe, Daten für ein Template so aufzubereiten, dass das phtml-File nur noch ausgeben muss. Weil ein ViewModel keine Abhängigkeit zu Context, Registry oder dem kompletten Block-Konstruktor-Graphen hat, lässt es sich isoliert instanziieren, isoliert testen und ohne Seiteneffekte in mehreren Blöcken wiederverwenden.

Diese Trennung zahlt sich vor allem bei wachsenden Projekten aus. Ohne Hyvä ViewModel-Disziplin wandert Business-Logik schleichend in Block-Klassen, in Layout-XML-Argumente oder sogar direkt ins phtml-Template, wo sie sich weder wiederverwenden noch isoliert testen lässt. Mit dem ViewModel-Pattern bleibt jede Regel an genau einer Stelle, mit einem klaren Konstruktor-Vertrag und ohne versteckte Kopplung an den Rendering-Zyklus von Magento.

2. Das ArgumentInterface: der Vertrag hinter jedem Hyvä ViewModel

Technisch gesehen ist ein Hyvä ViewModel nichts anderes als eine PHP-Klasse, die \Magento\Framework\View\Element\Block\ArgumentInterface implementiert. Dieses Interface existiert bereits im Magento-Core-Framework, es ist ein reines Marker-Interface ohne eine einzige Methode. Sein einziger Zweck: Der Objektmanager und die Argument-Auflösung in di.xml erkennen daran, dass eine Klasse als Block-Argument vom Typ "ViewModel" gültig ist.

Genau deshalb sollte niemand ein eigenes ViewModelInterface erfinden. Ein selbst geschriebenes Interface bricht die Kompatibilität mit Hyvä-Core-Templates, mit PHPStorm-Inspektionen für Hyvä-Projekte und mit jeder Konvention, die andere Module und Erweiterungen erwarten. Das ArgumentInterface aus dem Core ist bewusst leer gehalten, damit es als reiner Typmarker funktioniert, ohne eine Methode zu erzwingen, die für jeden denkbaren ViewModel-Anwendungsfall unpassend wäre.


<?php

declare(strict_types=1);

namespace Magento\Framework\View\Element\Block;

/**
 * Marker interface every Hyvä ViewModel must implement.
 * It intentionally declares no methods - it exists solely so that
 * di.xml can resolve the "view_model" argument against a stable, framework-owned contract.
 */
interface ArgumentInterface
{
}

// --------------------------------------------------------------------------

<?php

declare(strict_types=1);

namespace Vendor\Module\ViewModel;

use Magento\Framework\View\Element\Block\ArgumentInterface;

/**
 * Minimal Hyvä ViewModel skeleton.
 * No parent class, no coupling to the block or the template - just a plain
 * PHP class that fulfills the framework's ArgumentInterface contract.
 */
class ProductBadge implements ArgumentInterface
{
}

Weil ArgumentInterface keine Methoden vorschreibt, definiert jedes Hyvä ViewModel seine eigene öffentliche API frei nach fachlichem Bedarf. Das ist gewollt: Ein ViewModel für Produktpreise braucht andere Methoden als ein ViewModel für Kundenkontext. Die einzige Konvention, die Hyvä-Projekte einhalten sollten, ist ein sprechender Klassenname und ein Namensraum, der die Zuständigkeit klar erkennen lässt, etwa Vendor\Module\ViewModel\ProductBadge statt eines generischen Helper oder Util.

3. ViewModel registrieren: di.xml und $block->getViewModel()

Ein Hyvä ViewModel wird nicht im Konstruktor der Block-Klasse fest verdrahtet, sondern über di.xml als Block-Argument registriert. Dazu wird der Ziel-Block-Typ, der das Template rendert, mit einem Argument vom Namen view_model und dem xsi:type="object" konfiguriert. Magento löst dieses Argument beim Aufbau des Blocks über den Objektmanager auf und übergibt die fertige ViewModel-Instanz automatisch mit allen ihren eigenen Konstruktor-Abhängigkeiten.


<!-- File: app/code/Vendor/Module/etc/frontend/di.xml -->
<?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\Catalog\Block\Product\View">
        <arguments>
            <!-- Registers the Hyvä ViewModel as a constructor argument named "view_model" -->
            <argument name="view_model" xsi:type="object">Vendor\Module\ViewModel\ProductBadge</argument>
        </arguments>
    </type>
</config>

Im Template greift man auf das Hyvä ViewModel über $block->getViewModel() zu, obwohl die Block-Klasse selbst keine solche Methode definiert. Der Grund liegt in \Magento\Framework\DataObject, von dem jede Block-Klasse letztlich erbt: Der magische __call()-Mechanismus übersetzt getViewModel() automatisch in getData('view_model'), indem er den CamelCase-Methodennamen in Snake-Case umwandelt. Genau dieser Mechanismus liefert die per di.xml injizierte Instanz zurück, ganz ohne eigenen Getter in der Block-Klasse.


<?php
/** @var \Magento\Catalog\Block\Product\View $block */
/** @var \Magento\Framework\Escaper $escaper */
/** @var \Vendor\Module\ViewModel\ProductBadge $productBadge */
$productBadge = $block->getViewModel();
$sku = $block->getProduct()->getSku();
?>
<?php if ($productBadge->hasBadge($sku)): ?>
    <span class="inline-flex items-center rounded-full bg-orange-100 px-3 py-1 text-xs font-semibold text-orange-700">
        <?= $escaper->escapeHtml($productBadge->getBadgeLabel($sku)) ?>
    </span>
<?php endif; ?>

4. Praxisbeispiel: ein Hyvä ViewModel mit Repository-Injection

Der eigentliche Mehrwert eines Hyvä ViewModel zeigt sich erst, wenn es echte Abhängigkeiten wie ein Repository injiziert bekommt. Das folgende Beispiel lädt ein Produkt über ProductRepositoryInterface, prüft, ob es kürzlich angelegt wurde oder einen aktiven Sonderpreis hat, und liefert dem Template ein fertig formatiertes Badge-Label. Mit PHP 8.4 wird das über Constructor Property Promotion und readonly-Properties besonders knapp und lesbar.


<?php

declare(strict_types=1);

namespace Vendor\Module\ViewModel;

use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Locale\CurrencyInterface;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\Store\Model\StoreManagerInterface;

/**
 * Prepares product badge data (New / Sale) for the product view template.
 * All aggregation and formatting happens here - the phtml template only renders
 * the value this class returns.
 */
class ProductBadge implements ArgumentInterface
{
    private const NEW_PRODUCT_DAYS = 30;

    /**
     * @param ProductRepositoryInterface $productRepository Loads the product entity by SKU.
     * @param StoreManagerInterface $storeManager Provides the current store context.
     * @param CurrencyInterface $currency Formats prices in the current store currency.
     */
    public function __construct(
        private readonly ProductRepositoryInterface $productRepository,
        private readonly StoreManagerInterface $storeManager,
        private readonly CurrencyInterface $currency,
    ) {
    }

    /**
     * Determines whether the given product should show a badge at all.
     *
     * @param string $sku SKU of the product currently rendered by the block.
     * @return bool True if a badge label should be displayed.
     * @throws NoSuchEntityException If no product exists for the given SKU.
     */
    public function hasBadge(string $sku): bool
    {
        $product = $this->productRepository->get($sku);

        return $this->isNew($product) || $this->hasDiscount($product);
    }

    /**
     * Builds the human-readable badge label for the current product.
     *
     * @param string $sku SKU of the product currently rendered by the block.
     * @return string The badge text, e.g. "New" or "-20%".
     * @throws NoSuchEntityException If no product exists for the given SKU.
     */
    public function getBadgeLabel(string $sku): string
    {
        $product = $this->productRepository->get($sku);

        if ($this->hasDiscount($product)) {
            $percent = $this->calculateDiscountPercent($product);
            return sprintf('-%d%%', $percent);
        }

        return __('New')->render();
    }

    /**
     * Checks whether the product was created within the configured "new" window.
     *
     * @param ProductInterface $product The product entity to inspect.
     * @return bool True if the product counts as new.
     */
    private function isNew(ProductInterface $product): bool
    {
        $createdAt = strtotime((string) $product->getCreatedAt());
        $threshold = strtotime(sprintf('-%d days', self::NEW_PRODUCT_DAYS));

        return $createdAt !== false && $createdAt >= $threshold;
    }

    /**
     * Checks whether the product currently has a special price lower than its regular price.
     *
     * @param ProductInterface $product The product entity to inspect.
     * @return bool True if a special price discount is active.
     */
    private function hasDiscount(ProductInterface $product): bool
    {
        $specialPrice = (float) $product->getData('special_price');

        return $specialPrice > 0.0 && $specialPrice < (float) $product->getPrice();
    }

    /**
     * Calculates the discount percentage between regular price and special price.
     *
     * @param ProductInterface $product The product entity to inspect.
     * @return int Rounded discount percentage.
     */
    private function calculateDiscountPercent(ProductInterface $product): int
    {
        $price = (float) $product->getPrice();
        $specialPrice = (float) $product->getData('special_price');

        if ($price <= 0.0) {
            return 0;
        }

        return (int) round((1 - $specialPrice / $price) * 100);
    }
}

Auffällig ist, dass keine der drei injizierten Abhängigkeiten, ProductRepositoryInterface, StoreManagerInterface und CurrencyInterface, etwas mit Rendering zu tun hat. Genau das ist der Kern des Hyvä ViewModel-Gedankens: Die Klasse bekommt genau die fachlichen Bausteine, die sie zur Datenaufbereitung benötigt, per Constructor Property Promotion injiziert, ganz ohne den vollständigen Block-Konstruktor mit Context, Registry und einem Dutzend weiterer Legacy-Abhängigkeiten mitzuschleppen.

5. Präsentationslogik im ViewModel, reine Ausgabe im Template

Die zentrale Regel im Hyvä ViewModel-Pattern lautet: Formatierung, Aggregation und Sichtbarkeitsregeln gehören ins ViewModel, das phtml-Template darf nur noch das Ergebnis ausgeben. Ein typisches Anti-Pattern sieht dagegen so aus, dass ein Rabatt direkt im Template mit <?php $percent = round((1 - $special / $price) * 100); ?> berechnet wird. Damit landet Geschäftslogik in einer Datei, die weder von PHPStan sauber analysiert noch mit PHPUnit isoliert getestet werden kann.

Mit dem Hyvä ViewModel-Ansatz reduziert sich derselbe Codeabschnitt im Template auf einen einzigen Methodenaufruf: $productBadge->getBadgeLabel($sku). Jede Änderung an der Rabattlogik, etwa eine neue Rundungsregel oder ein zusätzliches Sichtbarkeitskriterium wie eine Store-View-Einschränkung, findet ausschließlich in der ViewModel-Klasse statt. Das Template bleibt unverändert, solange sich die öffentliche Methodensignatur nicht ändert, was Merge-Konflikte und Regressionstests im Frontend erheblich reduziert.

Diese Trennung erleichtert zusätzlich die Zusammenarbeit im Team: Frontend-Entwickler, die primär an Tailwind-Klassen und Alpine.js-Interaktionen arbeiten, müssen die Rabattberechnung nicht verstehen, um das Markup anzupassen. Backend-Entwickler, die die Geschäftsregel in einem Hyvä ViewModel ändern, riskieren keinen versehentlichen Bruch der Template-Struktur, weil sie ausschließlich in der PHP-Klasse arbeiten.

6. Mehrere ViewModels in einem Template kombinieren

In der Praxis reicht selten ein einziges Hyvä ViewModel pro Template. Eine Produktdetailseite braucht häufig ein ViewModel für Preisdaten, ein weiteres für Kundenkontext, etwa ob der eingeloggte Kunde eine spezielle Preisgruppe hat, und vielleicht noch eines für Lagerbestandsdarstellung. Da der Standard-Argumentname view_model pro Block nur einmal vergeben werden kann, braucht jedes weitere ViewModel einen eigenen, sprechenden Argumentnamen in der di.xml.

Namenskonflikte vermeidet man, indem jedes zusätzliche Hyvä ViewModel einen eigenen Argumentnamen wie price_view_model oder customer_context_view_model erhält. Dank desselben CamelCase-zu-Snake-Case-Mechanismus, der bereits getViewModel() auf view_model abbildet, wird price_view_model im Template über $block->getPriceViewModel() und customer_context_view_model über $block->getCustomerContextViewModel() erreichbar. So lassen sich beliebig viele ViewModels an einem einzigen Block registrieren, ohne dass sich ihre Zuständigkeiten überschneiden oder eine Instanz die andere überschreibt.

Eine bewährte Konvention ist, für jedes fachliche Konzept genau ein Hyvä ViewModel zu registrieren und keine Sammel-ViewModels zu bauen, die mehrere unabhängige Zuständigkeiten bündeln. Ein ViewModel, das gleichzeitig Preisformatierung, Kundenkontext und Lagerbestand verwaltet, verletzt dieselbe Single-Responsibility-Regel, die das Pattern eigentlich aus der Block-Klasse verbannen sollte, nur eine Ebene tiefer.

7. Testbarkeit: ein Hyvä ViewModel als reine PHP-Klasse

Der größte praktische Vorteil eines Hyvä ViewModel zeigt sich beim Testen. Eine Block-Klasse zu instanziieren, erfordert normalerweise einen kompletten Context-Objektgraphen mit Request, Layout, Cache-State und Dutzenden weiteren Kollaborateuren, was echte Unit-Tests praktisch unmöglich macht und die meisten Teams zu langsamen Integrationstests mit vollem Magento-Bootstrap zwingt. Ein ViewModel dagegen benötigt nur seine eigenen, im Konstruktor deklarierten Interfaces, die sich mit PHPUnit-Mocks vollständig ersetzen lassen.


<?php

declare(strict_types=1);

namespace Vendor\Module\Test\Unit\ViewModel;

use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\Locale\CurrencyInterface;
use Magento\Store\Model\StoreManagerInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Vendor\Module\ViewModel\ProductBadge;

/**
 * Verifies the badge logic of ProductBadge without booting the Magento framework.
 */
class ProductBadgeTest extends TestCase
{
    private ProductRepositoryInterface&MockObject $productRepository;
    private ProductBadge $viewModel;

    /**
     * Builds the ViewModel with mocked collaborators before each test.
     *
     * @return void
     */
    protected function setUp(): void
    {
        $this->productRepository = $this->createMock(ProductRepositoryInterface::class);
        $storeManager = $this->createMock(StoreManagerInterface::class);
        $currency = $this->createMock(CurrencyInterface::class);

        $this->viewModel = new ProductBadge(
            $this->productRepository,
            $storeManager,
            $currency,
        );
    }

    /**
     * Ensures a product with an active special price is reported as discounted.
     *
     * @return void
     */
    public function testHasBadgeReturnsTrueForDiscountedProduct(): void
    {
        $product = $this->createMock(ProductInterface::class);
        $product->method('getPrice')->willReturn(100.0);
        $product->method('getData')->with('special_price')->willReturn(80.0);
        $product->method('getCreatedAt')->willReturn('2020-01-01 00:00:00');

        $this->productRepository->method('get')->with('TEST-SKU')->willReturn($product);

        self::assertTrue($this->viewModel->hasBadge('TEST-SKU'));
        self::assertSame('-20%', $this->viewModel->getBadgeLabel('TEST-SKU'));
    }
}

Dieser Test läuft in Millisekunden, ohne Datenbankverbindung, ohne Magento-Bootstrap und ohne Testfixtures. Genau diese Geschwindigkeit macht den Unterschied im Entwickleralltag: Ein Hyvä ViewModel lässt sich bei jeder Änderung sofort verifizieren, während ein äquivalenter Test für dieselbe Logik in einer Block-Klasse fast immer auf teure Integrationstests mit Magento\TestFramework\TestCase\AbstractController ausweichen müsste.

8. Caching-Fallstricke: ViewModels und Block-HTML-Cache

Ein Hyvä ViewModel selbst hält in aller Regel keinen Zustand über eine Anfrage hinaus, das eigentliche Risiko liegt im Full-Page-Cache und im Block-HTML-Cache des Blocks, der das ViewModel referenziert. Rendert ein Block mit cacheable="true" in der Layout-XML ein Template, dessen ViewModel kundenspezifische Daten liefert, etwa individuelle Rabatte oder den Namen des eingeloggten Kunden, landet dieses personalisierte HTML im geteilten Cache und wird an den nächsten Besucher ausgeliefert.

Die Lösung liegt nicht im ViewModel selbst, sondern in der Cache-Konfiguration des Blocks. Entweder wird der Block mit cacheable="false" vom Full-Page-Cache ausgenommen, was bei stark frequentierten Seiten Performance kostet, oder die dynamischen Teile werden über Hyvä-typische Alpine.js-Komponenten nachträglich per privatem AJAX-Aufruf befüllt, während das restliche Markup weiterhin gecacht bleibt. Eine dritte Option ist, getCacheKeyInfo() auf der Block-Klasse so zu erweitern, dass die Kundengruppe oder eine andere Segmentierung Teil des Cache-Keys wird, was allerdings die Cache-Trefferquote reduziert.

Als Faustregel gilt: Ein Hyvä ViewModel, das nur produkt- oder katalogbezogene Daten liefert, ist unkritisch für den Block-Cache, weil dieselbe Antwort für alle Besucher gilt. Sobald ein ViewModel jedoch Kundensession, Warenkorbinhalt oder personalisierte Preise verarbeitet, muss die Cache-Strategie des umgebenden Blocks explizit überprüft werden, unabhängig davon, wie sauber das ViewModel selbst implementiert ist.

9. Migration: von der Block-Klasse zum ViewModel

Ein typisches Refactoring beginnt mit einer bestehenden Block-Klasse, die eine Methode wie getBadgeLabel() direkt implementiert und dabei ein Repository selbst aus dem Objektmanager zieht oder per Konstruktor injiziert bekommt, zusätzlich zum kompletten Context-Objektgraphen. Beim Umzug in ein Hyvä ViewModel wandert exakt diese Methode inklusive ihrer fachlichen Abhängigkeiten in die neue Klasse, während die Block-Klasse selbst unverändert bleiben kann, wenn sie ohnehin nur als Template-Wrapper diente.

Im Template ändert sich dabei lediglich der Zugriffspfad: Aus $block->getBadgeLabel($sku) wird $block->getViewModel()->getBadgeLabel($sku). Diese minimale Änderung macht die eigentliche Migration in bestehenden Projekten risikoarm, weil sich Template und Business-Logik unabhängig voneinander anpassen lassen. Die folgende Tabelle fasst die wichtigsten Unterschiede zwischen dem alten Block-Ansatz und dem neuen Hyvä ViewModel-Ansatz zusammen.

Aspekt Vorher: Block-Klasse Nachher: Hyvä ViewModel Vorteil
Datenaufbereitung Direkt in Block::getBadgeLabel() Ausgelagert in ProductBadge::getBadgeLabel() Single Responsibility je Klasse
Testbarkeit Voller Context, Registry, Bootstrap nötig Reine PHPUnit-Klasse mit Mocks Schnelle Unit-Tests ohne Magento
Wiederverwendung An eine einzige Block-Klasse gebunden Über di.xml an beliebigen Blocks registrierbar Keine Code-Duplizierung
Caching-Kontrolle Logik unsichtbar im Block-HTML-Cache Explizit über Block-Cache-Konfiguration steuerbar Kein stilles kundenspezifisches Caching
Kopplung Erbt vollen Block-Konstruktor-Graphen Implementiert nur ArgumentInterface Minimaler, expliziter Abhängigkeitsgraph

Nach der Migration bleibt die Block-Klasse in vielen Fällen fast leer, sie liefert dem Template nur noch den Produktkontext, während sämtliche Formatierungs- und Sichtbarkeitsregeln im Hyvä ViewModel stecken. Dieses schrittweise Refactoring lässt sich Methode für Methode durchführen, ohne dass ein komplettes Modul auf einmal umgeschrieben werden muss.

10. Zusammenfassung

Das Hyvä ViewModel-Pattern löst ein strukturelles Problem, das Luma-Templates jahrelang mit sich herumgetragen haben: Business-Logik in Block-Klassen, die weder isoliert testbar noch klar abgegrenzt war. Mit dem ArgumentInterface als leerem, framework-eigenem Vertrag, der Registrierung über di.xml als Block-Argument und dem Zugriff via $block->getViewModel() entsteht eine klare, wiederholbare Struktur für jede neue fachliche Regel im Frontend.

Die neun besprochenen Aspekte, von der Grundidee über Registrierung, Repository-Injection, die Trennung von Präsentationslogik und Ausgabe, mehrere ViewModels pro Template, Testbarkeit mit PHPUnit, Caching-Fallstricke bis zur konkreten Migration, bilden zusammen ein vollständiges Bild davon, wie ein Hyvä ViewModel im Projektalltag eingesetzt werden sollte. Wer diese Struktur konsequent durchzieht, reduziert Block-Klassen auf ihre eigentliche Aufgabe und gewinnt eine testbare, wiederverwendbare Business-Logik-Schicht.

Hyvä ViewModel-Pattern im Frontend: Das Wichtigste auf einen Blick

ArgumentInterface

Leeres Marker-Interface aus dem Core. Kein eigenes Interface erfinden, es bricht die Kompatibilität mit Hyvä-Konventionen.

di.xml Registrierung

Argument view_model vom Typ object am Block registrieren, Zugriff via $block->getViewModel().

Testbarkeit

Reine PHP-Klasse mit injizierten Interfaces, per PHPUnit ohne Magento-Bootstrap in Millisekunden testbar.

Caching beachten

Personalisierte ViewModel-Daten in cachefähigen Blocks bergen Risiko für stale, kundenspezifisches HTML.

11. FAQ: Hyvä ViewModel

1Was ist ein Hyvä ViewModel genau?
Eine PHP-Klasse, die ArgumentInterface implementiert und über di.xml als Block-Argument registriert wird. Sie bereitet Template-Daten auf, ohne Rendering-Logik zu enthalten.
2Warum kein eigenes ViewModelInterface?
ArgumentInterface ist bereits im Framework vorhanden und leer. Ein eigenes Interface bricht Kompatibilität zu Hyvä-Core-Templates und -Konventionen.
3Wie registriere ich ein ViewModel für einen Block?
Über di.xml mit einem Argument view_model vom xsi:type object am Ziel-Block-Typ, dessen Wert der volle ViewModel-Klassenname ist.
4Warum funktioniert getViewModel() ohne eigene Methode?
DataObject::__call() wandelt getViewModel() automatisch in getData('view_model') um und liefert die per di.xml registrierte Instanz zurück.
5Mehrere ViewModels an einem Block?
Ja, mit eigenen Argumentnamen wie price_view_model, erreichbar über getPriceViewModel() dank desselben Namensauflösungs-Mechanismus.
6Wie teste ich ein ViewModel mit PHPUnit?
Konstruktor-Abhängigkeiten per createMock() ersetzen und die Klasse direkt instanziieren, ganz ohne Magento-Bootstrap oder Datenbank.
7Ersetzt ein ViewModel die Block-Klasse komplett?
Meist ja für die Business-Logik. Die Block-Klasse bleibt ein schlanker Wrapper für Layout, Kind-Blöcke und Template-Verbindung.
8Caching bei kundenspezifischen ViewModel-Daten?
Block auf cacheable=false setzen, dynamische Teile per Alpine.js nachladen oder den Cache-Key über getCacheKeyInfo() erweitern.
9Braucht ein ViewModel immer ein Repository?
Nein. Reine Formatierungslogik ohne eigenen Datenzugriff ist ebenfalls ein gültiges ViewModel. Repository-Injection ist typisch, nicht zwingend.
10Wie migriere ich Block-Logik schrittweise?
Methode für Methode in die neue ViewModel-Klasse verschieben, im Template von $block->methode() auf $block->getViewModel()->methode() umstellen.

Mironsoft

Hyvä-Frontend-Architektur und Magento-2-Entwicklung

Ein Hyvä-Frontend, das sauber architektiert ist?

Wir analysieren bestehende Block-Klassen, ziehen Business-Logik konsequent in saubere ViewModels und bauen eine PHPUnit-Testsuite auf, die eure Hyvä ViewModel-Schicht dauerhaft absichert.

Code-Review

Audit bestehender Block- und ViewModel-Architektur nach Hyvä-Konventionen

Refactoring

Business-Logik aus Block-Klassen in testbare ViewModels auslagern

Testabdeckung

PHPUnit-Testsuiten für ViewModels ohne vollen Magento-Bootstrap