Ein "Meine Punkte"-Widget für CMS-Seiten bauen
Ein "Meine Punkte"-Widget für CMS-Seiten bauen
~7 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026
Kapitel 47 hat es bereits vorausgesagt: Widgets sind einer der drei Orte, an denen Magentos eigene Kernkonventionen eine echte Block-Unterklasse erzwingen, obwohl dieses Modul sonst konsequent ViewModels bevorzugt. Dieses Kapitel löst das konkret ein - und zeigt gleichzeitig, wie man trotzdem keine einzige Zeile Geschäftslogik dupliziert.
Warum hier kein ViewModel reicht
Das Muster aus Kapitel 51 - ein generischer Magento\Framework\View\Element\Template-Block mit view_model-Argument im Layout-XML - funktioniert nur, weil eine Layout-XML-Datei existiert, die diese Verdrahtung explizit deklariert. Sowohl die Widget-Instance-Verwaltung (Magento\Widget\Model\Widget\Instance) als auch der {{widget}}-Direktiven-Filter aus Kapitel 55 instanziieren die in widget.xml angegebene Klasse jedoch direkt über den Object Manager und rufen sofort toHtml() darauf auf - ohne Umweg über eine eigene Layout-XML-Datei und ohne view_model-Argument-Konzept. Die Klasse muss selbst der Block sein. Genau dafür markiert das leere \Magento\Widget\Block\BlockInterface eine Klasse als widget-tauglich - technisch ein reines Marker-Interface, exakt wie ArgumentInterface aus Kapitel 48, nur für einen anderen Zweck.
Die Widget-Klasse
<?php
declare(strict_types=1);
namespace Mironsoft\Loyalty\Model\Widget;
use Magento\Framework\View\Element\Template;
use Magento\Framework\View\Element\Template\Context;
use Magento\Widget\Block\BlockInterface;
use Mironsoft\Loyalty\ViewModel\PointsBalance;
/**
* Renders the "My Points" widget insertable into any CMS WYSIWYG content. Magento's widget
* subsystem (widget.xml, the {{widget}} directive filter) instantiates this class directly
* as a layout block - there is no view_model-argument equivalent here, so unlike every other
* template data source in this module (chapter 47), a real block subclass is unavoidable.
* The class still delegates every piece of actual logic to ViewModel\PointsBalance (chapter
* 48) instead of duplicating it - only the mandatory Template/BlockInterface wrapper is new.
*/
class PointsBalanceWidget extends Template implements BlockInterface
{
/**
* @param Context $context Framework template context, required by the parent Template class.
* @param PointsBalance $pointsBalanceViewModel Same view model already used in chapters 48/51/52 - reused unmodified here.
* @param array<string, mixed> $data Additional block data, populated by Magento's widget system from the admin-configured parameters (chapter 57).
*/
public function __construct(
Context $context,
private readonly PointsBalance $pointsBalanceViewModel,
array $data = [],
) {
parent::__construct($context, $data);
$this->setTemplate('Mironsoft_Loyalty::widget/points-balance.phtml');
}
/**
* Returns the injected view model. Exists purely so the template written in chapter 53 -
* which calls $block->getViewModel() - keeps working here completely unmodified, even
* though $block is now this widget class instead of a generic Template block.
*
* @return PointsBalance
*/
public function getViewModel(): PointsBalance
{
return $this->pointsBalanceViewModel;
}
}Genau eine neue Methode: getViewModel(). Der Rest ist reine Verdrahtung des Pflicht-Template-Konstruktors. Weil widget/points-balance.phtml aus Kapitel 53 bereits $block->getViewModel() aufruft, funktioniert genau dieselbe Template-Datei hier unverändert weiter - keine neue .phtml-Datei, keine duplizierte Business-Logik, nur der schmale BlockInterface-Wrapper, den Magentos Widget-System technisch verlangt.
Minimale widget.xml-Registrierung
<?xml version="1.0"?>
<widgets xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Widget:etc/widget.xsd">
<widget id="mironsoft_loyalty_points_balance"
class="Mironsoft\Loyalty\Model\Widget\PointsBalanceWidget"
is_email_compatible="false">
<label translate="true">Loyalty: My Points Balance</label>
<description translate="true">Displays the logged-in customer's points balance and loyalty tier.</description>
<parameters>
<!-- chapter 57 adds show_tier_badge and css_class here -->
</parameters>
</widget>
</widgets>Tipp: Wie bei ArgumentInterface in Kapitel 48 ist auch hier keine di.xml-Präferenz nötig - widget.xml ist die vollständige, eigenständige Registrierung. Nach dem Anlegen: bin/cache-clean config, damit das Widget im Admin unter Content > Elements > Widgets auftaucht (Kapitel 57 zeigt die Admin-Bedienung im Detail).
Achtung: Wo dieses Widget landet, ist keine beliebige Geschmacksfrage. Auf der bereits nicht-cachebaren Kontoseite (Kapitel 52, sitzungsgebunden) ist alles unkritisch. Wird dasselbe Widget aber per {{widget}}-Direktive in eine öffentliche, gewöhnlich vollständig gecachte CMS-Landingpage eingefügt, entsteht ein echtes Datenleck: Der Widget-Filter aus Kapitel 55 läuft, BEVOR Magentos Full-Page-Cache die fertige Seite als HTML einfriert - der Punktestand des ERSTEN Besuchers nach einer Cache-Invalidierung würde für alle nachfolgenden Besucher derselben Seite im Cache eingefroren bleiben, bis der Cache erneut invalidiert wird. Für öffentliche, cachebare Seiten ist Kapitel 84 (Customer Section Data, AJAX-basiert und FPC-sicher) der richtige Mechanismus, nicht dieses Widget. Kapitel 61 fasst diese Abwägung am Ende dieses Blocks noch einmal explizit zusammen.
Tipp: PointsBalance::canShow() (Kapitel 48) schützt weiterhin zuverlässig gegen Gäste und ein deaktiviertes Programm - das gilt unverändert für alle drei Einsatzorte dieses ViewModels: Widget (hier), Kontodashboard (Kapitel 52) und, mit dem in der Warnung oben genannten Cache-Vorbehalt, jede weitere CMS-Seite.