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

Helper Classes: When They Still Make Sense Despite the ViewModel Preference

Helper Classes: When They Still Make Sense Despite the ViewModel Preference

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

CLAUDE.md is unambiguous: ViewModels (ArgumentInterface) instead of block classes for anything template-related. No chapter in this series has contradicted that rule so far. This closing chapter of block 5 still honestly clarifies where the classic Magento helper class (extends \Magento\Framework\App\Helper\AbstractHelper) still has a place - not as a relapse into old habits, but for two narrowly scoped contexts a ViewModel simply doesn't cover.

What a helper historically was

AbstractHelper comes from the Luma/block era: inside a .phtml template, $this->helper(SomeHelper::class)->method() was the standard way to move logic out of the template. Hyvä deliberately doesn't use that pattern in storefront templates - this theme has no block-based $this->helper(), and CLAUDE.md rules out Luma, Knockout.js, and UI Components entirely. For anything a Hyvä storefront template needs, the ViewModel remains the right, and only, building block.

The two legitimate exceptions

1. Contexts outside the Hyva storefront

Transactional emails (Magento\Email\Model\Template) render through their own template engine, independent of Hyvä, even in a fully Hyvä shop - the same is true for large parts of the admin panel. A helper class that exclusively supplies a formatted points summary for the order confirmation email isn't competing with the ViewModel rule - it's simply the right tool for a context ViewModels never address in the first place.

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. Stateless utility functions with no Magento object graph

A pure formatting function with no configuration access, no database, no dependency at all doesn't need dependency injection - a plain, final-marked class with static methods is legitimate here and saves unnecessary object construction, for example in the console command from chapter 9, in the email helper above, and later in PDF output.

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, '.', ',');
    }
}

The rule of thumb

  • Logic for a Hyvä storefront template? → ViewModel (ArgumentInterface), always.
  • Domain logic with state and/or multiple dependencies? → service class, following the PointsCalculator pattern (chapter 5).
  • A pure, stateless function with no Magento object graph at all? → a static utility class is acceptable, but deliberately kept small and final.
  • Email templates, legacy admin context, or an AbstractHelper instance a third-party module expects? → a classic helper, documented as a deliberate exception outside the storefront.

Tipp: That wraps up block 5: two plugins (chapters 38/39), one cleanly justified preference (chapter 41) along with its risks (chapter 42), plugin ordering and conflicts (chapter 43), and this last open question about helper classes. Block 6 turns to the frontend starting in chapter 45: a custom controller, a custom router, and this module's first visible page.

Achtung: A helper or utility class that unnoticeably accumulates configuration reads, database calls, or other state over time turns right back into the global Mage_Core_Helper_Data anti-pattern that made Magento 1 infamous - exactly what separating ViewModels from service classes in this series has been preventing from the start. The moment a "utility" class needs a dependency, it's no longer a utility class - it's a candidate for a regular, injected service class.