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?
2. How do you access a ViewModel in a template?
3. What is the main benefit of ViewModels?
Flashcards
Question
What interface do ViewModels implement?
Click to reveal answer
Answer
Magento\Framework\View\Element\Block\ArgumentInterface
Question
How are ViewModels passed to blocks?
Click to reveal answer
Answer
Via layout XML <arguments> with xsi:type="object"
Question
Why use ViewModels over blocks?
Click to reveal answer
Answer
Clean separation, testability, reusability
Question
How do you test ViewModels?
Click to reveal answer
Answer
Unit test with mocked dependencies
Question
Can a block have multiple ViewModels?
Click to reveal answer
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();