Skip to content
intermediate Phase 54 · Frontend Components

ViewModels Deep Dive

Advanced ViewModel patterns in Magento 2: dependency injection, clean architecture, multiple view models, and testing

45m
0 problems
Topic Progress 0%

ViewModel Basics

What is a ViewModel?

ViewModels provide a clean way to pass data to templates without putting logic in blocks. They act as an intermediary between blocks and templates.

Creating a ViewModel

<?php
namespace Vendor\Module\ViewModel;

use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;

class ProductList implements ArgumentInterface
{
    public function __construct(
        private CollectionFactory $productCollectionFactory,
    ) {
    }

    public function getProducts(): array
    {
        return $this->productCollectionFactory->create()
            ->addFieldToSelect(['name', 'price', 'sku'])
            ->setPageSize(10)
            ->getItems();
    }

    public function getFormattedPrice(float $price): string
    {
        return sprintf('$%.2f', $price);
    }
}

Using ViewModel in Layout

<block class="Magento\Framework\View\Element\Template"
       name="product.list"
       template="product/list.phtml">
    <arguments>
        <argument name="product_view_model" xsi:type="object">
            Vendor\Module\ViewModel\ProductList
        </argument>
    </arguments>
</block>

Accessing ViewModel in Template

<!-- product/list.phtml -->
<?php
/** @var Vendor\Module\ViewModel\ProductList $viewModel */
$viewModel = $block->getProductViewModel();
$products = $viewModel->getProducts();
?>

<div class="product-list">
    <?php foreach ($products as $product): ?>
        <div class="product-item">
            <span><?= $block->escapeHtml($product->getName()) ?></span>
            <span><?= $viewModel->getFormattedPrice($product->getPrice()) ?></span>
        </div>
    <?php endforeach; ?>
</div>

Clean Architecture with ViewModels

Separation of Concerns

Block (Layout responsibility)
    ↓ provides data to
ViewModel (Business logic)
    ↓ provides data to
Template (Presentation)

ViewModel as ArgumentInterface

<?php
namespace Vendor\Module\ViewModel;

use Magento\Framework\View\Element\Block\ArgumentInterface;

class CartSummary implements ArgumentInterface
{
    public function __construct(
        private \Magento\Checkout\Model\Cart\CartInterface $cart,
        private \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency,
    ) {
    }

    public function getItemsCount(): int
    {
        return $this->cart->getItemsCount();
    }

    public function getSubtotal(): string
    {
        return $this->priceCurrency->format($this->cart->getSubtotal());
    }

    public function getItems(): array
    {
        return $this->cart->getItems();
    }
}

Block as Passthrough

<?php
namespace Vendor\Module\Block;

use Magento\Framework\View\Element\Template;

class CartSummary extends Template
{
    // Block only passes ViewModel data to template
    // No business logic in block
}

Benefits

  • Testability: ViewModels are easy to unit test
  • Reusability: Multiple blocks can use same ViewModel
  • Separation: Business logic separated from presentation
  • Dependency Injection: Clean DI in ViewModels

Multiple ViewModels

Using Multiple ViewModels

<block class="Magento\Framework\View\Element\Template"
       name="product.page"
       template="product/page.phtml">
    <arguments>
        <argument name="product_view_model" xsi:type="object">
            Vendor\Module\ViewModel\ProductInfo
        </argument>
        <argument name="price_view_model" xsi:type="object">
            Vendor\Module\ViewModel\PriceDisplay
        </argument>
        <argument name="review_view_model" xsi:type="object">
            Vendor\Module\ViewModel\ReviewSummary
        </argument>
    </arguments>
</block>

ViewModel Classes

<?php
namespace Vendor\Module\ViewModel;

use Magento\Framework\View\Element\Block\ArgumentInterface;

class ProductInfo implements ArgumentInterface
{
    public function __construct(
        private \Magento\Framework\Registry $registry,
    ) {
    }

    public function getProduct(): \Magento\Catalog\Model\Product
    {
        return $this->registry->registry('current_product');
    }
}

// PriceDisplay ViewModel
class PriceDisplay implements ArgumentInterface
{
    public function __construct(
        private \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency,
    ) {
    }

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

// ReviewSummary ViewModel
class ReviewSummary implements ArgumentInterface
{
    public function __construct(
        private \Magento\Review\Model\ResourceModel\Review\CollectionFactory $reviewCollection,
    ) {
    }

    public function getAverageRating(): float
    {
        // Calculate average rating
    }
}

Template Access

<!-- product/page.phtml -->
<?php
$productVM = $block->getProductViewModel();
$priceVM = $block->getPriceViewModel();
$reviewVM = $block->getReviewViewModel();

$product = $productVM->getProduct();
?>

<h1><?= $block->escapeHtml($product->getName()) ?></h1>
<div class="price">
    <?= $priceVM->formatPrice($product->getPrice()) ?>
</div>
<div class="rating">
    Average: <?= $reviewVM->getAverageRating() ?>/5
</div>

Testing ViewModels

Unit Testing ViewModels

<?php
namespace Vendor\Module\Test\Unit\ViewModel;

use PHPUnit\Framework\TestCase;
use Vendor\Module\ViewModel\CartSummary;
use Magento\Checkout\Model\Cart\CartInterface;
use Magento\Framework\Pricing\PriceCurrencyInterface;

class CartSummaryTest extends TestCase
{
    private $cartMock;
    private $priceCurrencyMock;
    private $viewModel;

    protected function setUp(): void
    {
        $this->cartMock = $this->createMock(CartInterface::class);
        $this->priceCurrencyMock = $this->createMock(PriceCurrencyInterface::class);
        
        $this->viewModel = new CartSummary(
            $this->cartMock,
            $this->priceCurrencyMock
        );
    }

    public function testGetItemsCount(): void
    {
        $this->cartMock->expects($this->once())
            ->method('getItemsCount')
            ->willReturn(5);

        $this->assertEquals(5, $this->viewModel->getItemsCount());
    }

    public function testGetSubtotal(): void
    {
        $this->cartMock->expects($this->once())
            ->method('getSubtotal')
            ->willReturn(100.00);

        $this->priceCurrencyMock->expects($this->once())
            ->method('format')
            ->with(100.00)
            ->willReturn('$100.00');

        $this->assertEquals('$100.00', $this->viewModel->getSubtotal());
    }
}

Integration Testing

public function testViewModelWithRealData(): void
{
    $objectManager = $this->getObjectManager();
    
    $viewModel = $objectManager->create(CartSummary::class);
    
    $this->assertIsInt($viewModel->getItemsCount());
    $this->assertIsString($viewModel->getSubtotal());
}

Benefits of Testing ViewModels

  • Isolation: Test business logic without block/template
  • Speed: Unit tests are fast
  • Coverage: Easy to achieve high test coverage
  • Maintenance: Changes in ViewModel don't affect tests

Quiz

1. What interface must ViewModels implement?

Question 1 options

2. How do you access a ViewModel in a template?

Question 2 options

3. What is the main benefit of ViewModels?

Question 3 options

Flashcards

Question

What interface do ViewModels implement?

Answer

Magento\Framework\View\Element\Block\ArgumentInterface

Question

How are ViewModels passed to blocks?

Answer

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

Question

Why use ViewModels over blocks?

Answer

Clean separation, testability, reusability

Question

How do you test ViewModels?

Answer

Unit test with mocked dependencies

Question

Can a block have multiple ViewModels?

Answer

Yes, pass multiple arguments in layout XML

Revision Notes

Key Takeaways

  • 1. ViewModels implement ArgumentInterface for layout injection
  • 2. They provide clean separation between logic and presentation
  • 3. Multiple ViewModels can be used per block
  • 4. ViewModels are easy to unit test
  • 5. Blocks become simple passthrough classes

Interview Tips

  • Explain the ViewModel pattern vs traditional blocks
  • Know the ArgumentInterface requirement
  • Discuss when to use ViewModels vs blocks
  • Be ready to create and test ViewModels

Cheat Sheet

ViewModel:
- Implements ArgumentInterface
- Passed via layout XML arguments
- Accessible in template via block getter

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

Template:
$viewModel = $block->getVm();