Why ViewModel Over Block?
Problems with Blocks
Traditional Magento blocks mix two concerns:
- Layout/rendering logic — template selection, child blocks, caching
- 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
- Testability — ViewModels can be unit tested without layout context
- Reusability — Same ViewModel can power different blocks/templates
- Cleaner templates — Logic delegated to ViewModel methods
- 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?
2. How do you inject a ViewModel into a block via layout XML?
3. How do you access the ViewModel in a template?
Flashcards
Question
What does a ViewModel handle?
Click to reveal answer
Answer
Business logic that blocks previously handled: data fetching, formatting, computation
Question
How do you connect a ViewModel to a block?
Click to reveal answer
Answer
Via layout XML arguments with xsi:type="object"
Question
Why are ViewModels easier to test?
Click to reveal answer
Answer
They don't depend on the layout system — just mock the constructor dependencies
Question
What should a block do with a ViewModel?
Click to reveal answer
Answer
Store it and expose via getViewModel() for templates to use
Question
Can a block have multiple ViewModels?
Click to reveal answer
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();