ddtcorex

03 Sept 2026 · 3 min read

Hyva view models: moving logic to PHP

How Hyva view models keep display logic in testable PHP instead of the browser — what they replace from Knockout, how to write one, and when to reach for them.

Part 8 of the Hyva series — building the core storefront. Implementation-focused.

The problem view models solve

In Luma, the logic that decided what to show — is this product low on stock, what label to render, which price tier applies — often ended up split between a Knockout view model in JavaScript and conditional noise inside the PHTML. That split is hard to test (it runs in the browser) and hard to read (the rule is buried in bindings).

Hyva's answer is the PHP view model: a plain class that computes display state on the server, where your test suite already runs. The template calls methods and stays declarative.

Writing a view model

A view model implements Hyva\Theme\ViewModel\ViewModelInterface and is injected like any Magento dependency:

<?php
declare(strict_types=1);

namespace ExampleStore\Theme\ViewModel;

use Hyva\Theme\ViewModel\ViewModelInterface;
use Magento\Catalog\Model\Product;
use Magento\InventorySalesApi\Api\GetProductSalableQtyInterface;

class StockBadge implements ViewModelInterface
{
    public function __construct(
        private GetProductSalableQtyInterface $salableQty
    ) {}

    public function getBadge(Product $product): ?string
    {
        $qty = $this->salableQty->execute(
            $product->getSku(),
            (int) $product->getStore()->getWebsiteId()
        );

        return match (true) {
            $qty <= 0 => 'out-of-stock',
            $qty <= 5 => 'low-stock',
            default => null,
        };
    }
}

The rule lives in PHP, takes a real product, and returns a simple string. You can unit-test it without a browser.

Wiring it into layout and template

Declare the view model in layout XML as a block argument or a standalone virtual type, then pass it to the template:

<block class="Magento\Framework\View\Element\Template"
       name="product.stock.badge"
       template="ExampleStore_Theme::product/stock-badge.phtml">
    <arguments>
        <argument name="view_model" xsi:type="object">ExampleStore\Theme\ViewModel\StockBadge</argument>
    </arguments>
</block>

In the template, call it:

<?php /** @var \ExampleStore\Theme\ViewModel\StockBadge $viewModel */ ?>
<?php $badge = $viewModel->getBadge($block->getProduct()); ?>
<?php if ($badge === 'low-stock'): ?>
  <span class="rounded bg-amber-100 px-2 py-1 text-xs font-medium text-amber-800">Low stock</span>
<?php endif; ?>

There is no JavaScript computing this. The badge is correct on first paint, and the logic is covered by your PHP tests.

What this replaces

Luma / Knockout Hyva view model
uiElement view model in JS Plain PHP class
Logic in browser bindings Logic on the server
Hard to unit-test Standard PHPUnit
Conditional soup in PHTML Declarative template calls

Not everything belongs in a view model. Pure presentation state — is this dropdown open, what is the current tab — stays in Alpine, because it is ephemeral UI state that never touches the catalog. The line is: catalog/business rules in PHP, component UI state in Alpine.

Pitfalls

  • Don't put HTTP calls in a view model during render. If a rule needs live data, fetch it from Alpine after render (see the Alpine post) so the server response stays fast.
  • Keep view models thin. They orchestrate domain services; they should not contain business logic themselves. Delegate to the existing Magento APIs.
  • Type the return values. A view model that returns mixed forces the template to guess. Return explicit strings, booleans, or small value objects.

What's next

So far the series has built a theme, laid out pages, wired interactivity, and moved logic to PHP. The next post confronts the part of a real migration that textbooks skip: the third-party modules that only ship Luma templates, and how to write the Hyva compatibility layer for them.

Next in this series: Hyva compatibility modules for third-party extensions — /blog/magento2-hyva-compatibility-modules

magento2hyvafrontendphp