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

Block vs. View Model: When to Use Which

Block vs. View Model: When to Use Which

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

CLAUDE.md is unambiguous: "Prefer ViewModels (ArgumentInterface) over block classes." So far that rule has stayed abstract - no chapter has needed a template data source. That changes with the history page (chapter 45) and the reward catalog (chapter 49): both templates need data that has to come from somewhere. This chapter puts both approaches side by side concretely, before chapter 48 builds the view model actually used.

The classic block approach (not this series' goal)

Here's what a points balance display would look like following the classic Luma pattern - pure illustration, this code is never actually added anywhere in this module:

// NOT part of this module - pure counter-example
class PointsBalance extends \Magento\Framework\View\Element\Template
{
    public function __construct(
        \Magento\Framework\View\Element\Template\Context $context,
        private readonly \Magento\Customer\Model\Session $customerSession,
        private readonly \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository,
        array $data = []
    ) {
        parent::__construct($context, $data);
    }

    public function getPointsBalance(): int
    {
        // Business logic right inside the block - the first step toward a "god object"
        $customer = $this->customerRepository->getById(
            (int) $this->customerSession->getCustomerId()
        );
        $attribute = $customer->getCustomAttribute('loyalty_points_balance');

        return $attribute !== null ? (int) $attribute->getValue() : 0;
    }
}

Achtung: The obvious flaw here isn't the business logic itself - getCustomAttribute() is the same service-contract-compliant access the view model approach uses too. The flaw is the coupling to \Magento\Framework\View\Element\Template: a unit test would have to construct or mock the entire Context with all its own dependencies (URL builder, layout, event manager, ...) just to test getPointsBalance(). Chapter 91 (unit tests) describes exactly this problem in detail using PointsCalculator as its example - blocks are, in practice, barely testable in isolation.

The view model approach (from chapter 48 in this module)

An ArgumentInterface view model has no base class, no Context, no coupling to the view layer's object model at all - it's an ordinary PHP object injected via constructor property promotion:

// Preview of chapter 48 - full class there
class PointsBalance implements \Magento\Framework\View\Element\Block\ArgumentInterface
{
    public function __construct(
        private readonly \Magento\Customer\Model\Session $customerSession,
        private readonly \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository,
    ) {
    }

    public function getPointsBalance(): int { /* ... */ }
}

This still ends up with a <block> tag in the layout XML (full version in chapter 51) - and that's the point many beginners miss. The difference: the block class is the generic core class Magento\Framework\View\Element\Template itself, not a custom subclass. The view model is merely injected into it as a view_model argument and read from the template via $block->getViewModel():

<!-- Preview of chapter 51 -->
<block class="Magento\Framework\View\Element\Template"
       name="loyalty.points.balance"
       template="Mironsoft_Loyalty::widget/points-balance.phtml">
    <arguments>
        <argument name="view_model" xsi:type="object">Mironsoft\Loyalty\ViewModel\PointsBalance</argument>
    </arguments>
</block>

Side by side

  • Testability: view model - new PointsBalance($sessionMock, $repositoryMock) is enough for a unit test. Block - the full Template\Context has to be built too.
  • Reusability: view model - the same view model can be plugged into several blocks/layout handles (chapter 52 reuses PointsBalance a second time on the dashboard). Block - a subclass is bound to exactly one template ecosystem.
  • Coupling: view model - only knows what's injected via its constructor. Block - automatically inherits the entire Template API (URL building, layout access, child blocks), even when none of it is needed.
  • Magento convention since 2.2: the view model is the officially recommended way to expose pure display data. Blocks remain the technical requirement for anything Magento genuinely expects as an AbstractBlock instance in the layout tree at runtime.

When a block is still needed

This series doesn't contradict the view model rule anywhere - but three later blocks honestly show where Magento's own core conventions force a real block subclass, because the framework genuinely expects a fixed base class at that exact point:

  • Widget (chapters 55-57): widget.xml directly instantiates a \Magento\Widget\Block\BlockInterface implementation.
  • Page Builder content type (chapters 58-60): the admin preview template runs through a block class Page Builder itself dictates.
  • Payment/shipping method block (chapters 62-69): the checkout method renderer registration expects a block class that extends the base class the respective core module dictates.

Tipp: The rule of thumb from chapter 44 still applies unchanged: a view model is the default for anything a Hyvä storefront template needs in the way of pure display data. A block isn't an equivalent alternative you "could also pick" - it's only justified where Magento itself technically requires an AbstractBlock instance. Chapter 48 now builds this block's first real view model.