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

Helper-Klassen: wann sie trotz ViewModel-Präferenz noch sinnvoll sind

Helper-Klassen: wann sie trotz ViewModel-Präferenz noch sinnvoll sind

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

CLAUDE.md ist eindeutig: ViewModels (ArgumentInterface) statt Block-Klassen für alles, was ein Template betrifft. Kein Kapitel dieser Serie hat dieser Regel bisher widersprochen. Dieses letzte Kapitel von Block 5 klärt trotzdem ehrlich, wo die klassische Magento-Helper-Klasse (extends \Magento\Framework\App\Helper\AbstractHelper) noch ihren Platz hat - nicht als Rückfall in alte Gewohnheiten, sondern für zwei eng begrenzte Kontexte, die ein ViewModel schlicht nicht abdeckt.

Was ein Helper historisch war

AbstractHelper stammt aus der Luma-/Block-Ära: in einem .phtml-Template war $this->helper(SomeHelper::class)->method() der Standardweg, Logik aus dem Template auszulagern. Hyvä nutzt dieses Muster in Storefront-Templates bewusst nicht - es gibt in diesem Theme kein blockbasiertes $this->helper(), und CLAUDE.md schließt Luma, Knockout.js und UI-Components ohnehin aus. Für alles, was ein Hyvä-Storefront-Template braucht, bleibt das ViewModel der richtige, einzige Baustein.

Die zwei legitimen Ausnahmen

1. Kontexte außerhalb des Hyvä-Storefronts

Transaktions-E-Mails (Magento\Email\Model\Template) werden auch in einem vollständigen Hyvä-Shop über eine eigene, von Hyvä unabhängige Template-Engine gerendert - dasselbe gilt für viele Admin-Bereiche. Eine Helper-Klasse, die ausschließlich für die Bestellbestätigungs-Mail eine formatierte Punkte-Zusammenfassung liefert, konkurriert nicht mit der ViewModel-Regel - sie ist schlicht das richtige Werkzeug für einen Kontext, den ViewModels gar nicht adressieren.

app/code/Mironsoft/Loyalty/Helper/EmailPointsHelper.php
<?php

declare(strict_types=1);

namespace Mironsoft\Loyalty\Helper;

use Magento\Framework\App\Helper\AbstractHelper;
use Magento\Framework\App\Helper\Context;
use Mironsoft\Loyalty\Model\Config\LoyaltyConfig;
use Mironsoft\Loyalty\Model\Util\PointsFormatter;

/**
 * Formats loyalty points text for transactional email templates, which
 * render outside the Hyva storefront and therefore outside ViewModel scope.
 */
class EmailPointsHelper extends AbstractHelper
{
    /**
     * @param Context $context Framework helper context, required by AbstractHelper.
     * @param LoyaltyConfig $loyaltyConfig Reads the points-per-euro rate for display context.
     */
    public function __construct(
        Context $context,
        private readonly LoyaltyConfig $loyaltyConfig,
    ) {
        parent::__construct($context);
    }

    /**
     * Builds the "you earned N points" sentence used in the order confirmation email.
     *
     * @param int $pointsEarned Points earned on this order.
     * @return string
     */
    public function getEarnedPointsText(int $pointsEarned): string
    {
        if ($pointsEarned <= 0 || !$this->loyaltyConfig->isEnabled()) {
            return '';
        }

        return (string) __('You earned %1 points with this order.', PointsFormatter::formatPoints($pointsEarned));
    }
}

2. Zustandslose Utility-Funktionen ohne Magento-Objektgraph

Eine reine Formatierungsfunktion ohne Konfigurationszugriff, ohne Datenbank, ohne jede Abhängigkeit braucht keine Dependency Injection - eine schlichte, final markierte Klasse mit statischen Methoden ist hier legitim und spart unnötigen Objektaufbau, etwa im Konsolenbefehl aus Kapitel 9, in der E-Mail-Helper-Klasse oben und später in PDF-Ausgaben.

app/code/Mironsoft/Loyalty/Model/Util/PointsFormatter.php
<?php

declare(strict_types=1);

namespace Mironsoft\Loyalty\Model\Util;

/**
 * Stateless number-formatting utility for loyalty points. Deliberately final -
 * a static utility class has no legitimate reason to be subclassed (see the
 * final-method discussion in chapters 40-41 for the mirror-image argument).
 */
final class PointsFormatter
{
    /**
     * Formats a point count with a thousands separator, e.g. "1.250".
     *
     * @param int $points Point count to format.
     * @return string
     */
    public static function formatPoints(int $points): string
    {
        return \number_format($points, 0, ',', '.');
    }
}

Faustregel

  • Logik für ein Hyvä-Storefront-Template? → ViewModel (ArgumentInterface), immer.
  • Domänenlogik mit Zustand und/oder mehreren Abhängigkeiten? → Service-Klasse nach dem Muster von PointsCalculator (Kapitel 5).
  • Reine, zustandslose Funktion ganz ohne Magento-Objektgraph? → statische Utility-Klasse zulässig, aber bewusst klein und final gehalten.
  • E-Mail-Templates, Admin-Legacy-Kontext oder eine von einem Drittanbieter-Modul erwartete AbstractHelper-Instanz? → klassischer Helper, dokumentiert als bewusste Ausnahme außerhalb des Storefronts.

Tipp: Block 5 ist damit abgeschlossen: zwei Plugins (Kapitel 38/39), eine sauber begründete Preference (Kapitel 41) samt ihrer Risiken (Kapitel 42), Plugin-Reihenfolge und -Konflikte (Kapitel 43) und die letzte offene Frage zu Helper-Klassen. Block 6 wendet sich ab Kapitel 45 dem Frontend zu: eigene Controller, ein eigener Router und die erste sichtbare Seite dieses Moduls.

Achtung: Eine Helper- oder Utility-Klasse, die über die Zeit unbemerkt Konfigurationszugriffe, Datenbankaufrufe oder sonstigen Zustand ansammelt, verwandelt sich zurück in genau das globale Mage_Core_Helper_Data-Antimuster, das Magento 1 berüchtigt gemacht hat - und das die Trennung von ViewModel und Service-Klasse in dieser Serie von Anfang an verhindern soll. Sobald eine "Utility"-Klasse eine Abhängigkeit braucht, ist sie keine Utility-Klasse mehr, sondern ein Kandidat für eine reguläre, injizierte Service-Klasse.