Skip to content
intermediate Phase 24 · Module Frontend

ViewModel Pattern - Separating Logic from Blocks

Understanding the Magento 2 ViewModel pattern: separating business logic from blocks, DI in view models, and writing clean template code

45m
0 problems
Topic Progress 0%

Why ViewModel Over Block?

Problems with Blocks

Traditional Magento blocks mix two concerns:

  1. Layout/rendering logic — template selection, child blocks, caching
  2. Business logic — data fetching, formatting, computation

This makes blocks:

  • Hard to test (tightly coupled to layout system)
  • Difficult to reuse (depend on layout context)
  • Full of constructor dependencies (DB, models, services)

The ViewModel Solution

ViewModels separate concerns:

Block: handles layout/rendering (thin)
ViewModel: handles business logic (testable)
Template: uses ViewModel for data

Architecture Comparison

Traditional Block

// Block does everything
class ProductList extends Template
{
    public function __construct(
        Template\Context $context,
        CollectionFactory $collectionFactory,
        PriceHelper $priceHelper,
        CustomerSession $session,
        array $data = []
    ) {
        parent::__construct($context, $data);
    }

    public function getProducts(): array { /* ... */ }
    public function formatPrice($price) { /* ... */ }
    public function isLoggedIn(): bool { /* ... */ }
}

ViewModel Pattern

// Block is thin
class ProductList extends Template
{
    public function __construct(
        Template\Context $context,
        private ProductListViewModel $viewModel,
        array $data = []
    ) {
        parent::__construct($context, $data);
    }

    public function getViewModel(): ProductListViewModel
    {
        return $this->viewModel;
    }
}

// ViewModel handles logic
class ProductListViewModel
{
    public function __construct(
        private CollectionFactory $collectionFactory,
        private PriceHelper $priceHelper,
        private CustomerSession $session
    ) {}

    public function getProducts(): array { /* ... */ }
    public function formatPrice($price) { /* ... */ }
    public function isLoggedIn(): bool { /* ... */ }
}

Benefits

  1. Testability — ViewModels can be unit tested without layout context
  2. Reusability — Same ViewModel can power different blocks/templates
  3. Cleaner templates — Logic delegated to ViewModel methods
  4. Better DI — ViewModels get dependencies through constructor, not layout

Creating ViewModels

ViewModel Class

<?php
namespace Amazon\Prep\ViewModel;

use Amazon\Prep\Model\ResourceModel\Item\CollectionFactory;
use Amazon\Prep\Helper\PriceFormatter;
use Magento\Customer\Model\Session as CustomerSession;

class PrepDashboard
{
    public function __construct(
        private CollectionFactory $collectionFactory,
        private PriceFormatter $priceFormatter,
        private CustomerSession $customerSession
    ) {}

    public function getRecentItems(): array
    {
        return $this->collectionFactory->create()
            ->addFieldToSelect('*')
            ->setOrder('created_at', 'DESC')
            ->setPageSize(5)
            ->getItems();
    }

    public function getItemCount(): int
    {
        return (int)$this->collectionFactory->create()->getSize();
    }

    public function formatPrice(float $price): string
    {
        return $this->priceFormatter->format($price);
    }

    public function getCustomerName(): string
    {
        return $this->customerSession->getCustomer()->getName();
    }

    public function isLoggedIn(): bool
    {
        return $this->customerSession->isLoggedIn();
    }
}

Block Class (Thin)

<?php
namespace Amazon\Prep\Block;

use Magento\Framework\View\Element\Template;
use Amazon\Prep\ViewModel\PrepDashboard as PrepDashboardViewModel;

class PrepDashboard extends Template
{
    public function __construct(
        Template\Context $context,
        private PrepDashboardViewModel $viewModel,
        array $data = []
    ) {
        parent::__construct($context, $data);
    }

    public function getViewModel(): PrepDashboardViewModel
    {
        return $this->viewModel;
    }
}

Connecting ViewModel to Block via Layout XML

<block class="Amazon\Prep\Block\PrepDashboard"
       name="prep.dashboard"
       template="dashboard.phtml">
    <arguments>
        <argument name="viewModel" xsi:type="object">
            Amazon\Prep\ViewModel\PrepDashboard
        </argument>
    </arguments>
</block>

The xsi:type="object" tells DI to inject the ViewModel instance.

Using ViewModels in Templates

Template with ViewModel

<!-- dashboard.phtml -->
<?php
/** @var Amazon\Prep\Block\PrepDashboard $block */
$viewModel = $block->getViewModel();
?>
<div class="prep-dashboard">
    <h1><?= $viewModel->isLoggedIn()
        ? __('Welcome, %1', $viewModel->getCustomerName())
        : __('Prep Dashboard')
    ?></h1>

    <div class="stats">
        <span class="count">
            <?= __('%1 items', $viewModel->getItemCount()) ?>
        </span>
    </div>

    <div class="recent-items">
        <h2><?= __('Recent Items') ?></h2>
        <?php foreach ($viewModel->getRecentItems() as $item): ?>
            <div class="item">
                <span class="name">
                    <?= $block->escapeHtml($item->getName()) ?>
                </span>
                <span class="price">
                    <?= $viewModel->formatPrice($item->getPrice()) ?>
                </span>
            </div>
        <?php endforeach; ?>
    </div>
</div>

Multiple ViewModels

A block can have multiple ViewModels:

class ProductPage extends Template
{
    public function __construct(
        Template\Context $context,
        private ProductViewModel $productViewModel,
        private WarrantyViewModel $warrantyViewModel,
        private RecommendationViewModel $recommendationViewModel,
        array $data = []
    ) {
        parent::__construct($context, $data);
    }
}

Layout XML:

<block class="Amazon\Prep\Block\ProductPage"
       name="product.page"
       template="product/page.phtml">
    <arguments>
        <argument name="productViewModel" xsi:type="object">
            Amazon\Prep\ViewModel\Product
        </argument>
        <argument name="warrantyViewModel" xsi:type="object">
            Amazon\Prep\ViewModel\Warranty
        </argument>
        <argument name="recommendationViewModel" xsi:type="object">
            Amazon\Prep\ViewModel\Recommendation
        </argument>
    </arguments>
</block>

Testing ViewModels

ViewModels are easy to unit test:

use PHPUnit\Framework\TestCase;
use Amazon\Prep\ViewModel\PrepDashboard;

class PrepDashboardTest extends TestCase
{
    public function testGetItemCount(): void
    {
        $collection = $this->createMock(Collection::class);
        $collection->method('getSize')->willReturn(42);

        $factory = $this->createMock(CollectionFactory::class);
        $factory->method('create')->willReturn($collection);

        $viewModel = new PrepDashboard(
            $factory,
            $this->createMock(PriceFormatter::class),
            $this->createMock(CustomerSession::class)
        );

        $this->assertEquals(42, $viewModel->getItemCount());
    }
}

Quiz

1. What is the main benefit of the ViewModel pattern over traditional blocks?

Question 1 options

2. How do you inject a ViewModel into a block via layout XML?

Question 2 options

3. How do you access the ViewModel in a template?

Question 3 options

Flashcards

Question

What does a ViewModel handle?

Answer

Business logic that blocks previously handled: data fetching, formatting, computation

Question

How do you connect a ViewModel to a block?

Answer

Via layout XML arguments with xsi:type="object"

Question

Why are ViewModels easier to test?

Answer

They don't depend on the layout system — just mock the constructor dependencies

Question

What should a block do with a ViewModel?

Answer

Store it and expose via getViewModel() for templates to use

Question

Can a block have multiple ViewModels?

Answer

Yes, inject multiple ViewModels as separate constructor arguments

Revision Notes

Key Takeaways

  • 1. ViewModels separate business logic from block rendering concerns
  • 2. Blocks become thin wrappers that expose ViewModels to templates
  • 3. ViewModels are injected via layout XML arguments with xsi:type="object"
  • 4. ViewModels are easy to unit test without layout context
  • 5. Templates access ViewModels via $block->getViewModel()

Interview Tips

  • Explain the ViewModel pattern and why it improves on blocks
  • Know how to connect ViewModels via layout XML
  • Be ready to demonstrate testing a ViewModel
  • Discuss when to use ViewModel vs traditional block approach

Cheat Sheet

ViewModel: business logic (testable)
Block: layout/rendering (thin)

Connect via layout XML:
  <argument name="vm" xsi:type="object">
    Vendor\Module\ViewModel\Name
  </argument>

Template:
  $vm = $block->getViewModel();
  $vm->getData();