ViewModel używany jest w Magento 2.3 w celu oddzielenia logiki biznesowej od klas Bloków. Dzięki temu zamiast rozszeszać istniejące bloki w celu dodania nowej logiki biznesowej, zaleca się tworzyć ViewModele i je wstrzykiać do bloków.

Przykład który pokazuje jak bardzo fajne są ViewModele

Być może ten przykład będzie dość ekstremalny, pomimo tego chcę abyś dobrze zrozumiał o co mi chodzi.

Założenie: muszę utworzyć nowy blok, który dziedziczy po \Magento\Catalog\Block\Product\View i chcę dodać tam jakąś nową logikę biznesową, która wiąże się z dodaniem nowych zależności do klasy.

Gdy nie znam ViewModeli to robię to tak:

  1. Dodaję blok w layout XML. Ustawiam odpowiedną klasę i templatkę
  2. Deklaruje nową klasę, która dziedziczy po \Magento\Catalog\Block\Product\View
  3. Siedzę i płaczę bo widzę ile ta klasa ma zależności
class Banner extends View
{
    /**
     * @var BannerRepository
     */
    private $bannerRepository;

    public function __construct(
        Context $context,
        \Magento\Framework\Url\EncoderInterface $urlEncoder,
        \Magento\Framework\Json\EncoderInterface $jsonEncoder,
        \Magento\Framework\Stdlib\StringUtils $string,
        \Magento\Catalog\Helper\Product $productHelper,
        \Magento\Catalog\Model\ProductTypes\ConfigInterface $productTypeConfig,
        \Magento\Framework\Locale\FormatInterface $localeFormat,
        \Magento\Customer\Model\Session $customerSession,
        ProductRepositoryInterface $productRepository,
        \Magento\Framework\Pricing\PriceCurrencyInterface
        $priceCurrency,
        BannerRepository $bannerRepository,
        array $data = []
    ) {
        parent::__construct($context,
            $urlEncoder,
            $jsonEncoder,
            $string,
            $productHelper,
            $productTypeConfig,
            $localeFormat,
            $customerSession,
            $productRepository,
            $priceCurrency,
            $data
        );
        
        $this->bannerRepository = $bannerRepository;
    }
}

Jak widzisz potrzebowałem dodać jedną zależność bannerRepository (linia 20 i 36). Niestety przez to, ze dziedziczę po innej klasie mam tutaj masę zależności z kasy-rodzica. Nie wygląda to najlepiej, Ciężko tak developować, ciężko tak testować, źle, wszystko źle.

ViewModel na ratunek, czyli praktyczny przykład wykorzystania wzorca Composition over inheritance

Teraz pokażę Ci jak to się robi przy pomocy ViewModelu. Pokazuję na module CatalogBanners, który rozwijam w ramach kursu Modyfikacja listy produktów – praktyczny poradnik dla Magento Developerów

Definicja bloku w layout XML

Blok chcę umieścić na stronie kategorii, tuż za listą produktów. Tworzę plik Mkwiatkowski/CatalogBanners/view/frontend/layout/catalog_category_view.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="content">
            <block name="category.banners"
                   template="Mkwiatkowski_CatalogBanners::category/banner.phtml"
                   after="category.products">
                <arguments>
                    <argument name="view_model" xsi:type="object">Mkwiatkowski\CatalogBanners\ViewModel\CatalogBanners</argument>
                </arguments>
            </block>
        </referenceContainer>
    </body>
</page>

W szóstej linii deklaruję blok. Zauważ, że nie definiuje tam klasy tego bloku (atrybut class). W takim przypadku Magento wyrenderuje blok o klasie Magento/Framework/View/Element/Template. Jeśli chcesz inny blok to podajesz tą klasę jawnie w atrybucie class, jeśli nie podasz to Magento wyświetli defaultowy.

W linii jedenastej wstrzykuję do bloku ViewModel. Sprowadza się to d przekazania argumentu view_model, w którym przekazujemy ścieżkę do klasy ViewModelu.

Klasa ViewModelu i logika biznesowa

Czas na utworzenie klasy ViewModelu. Tworzę plik Mkwiatkowski/CatalogBanners/ViewModel/CatalogBanners.php. Przyjęło się, ze ViewModele tworzy się w katalogu ViewModel… zaskakujące prawda?

<?php
declare(strict_types=1);
namespace Mkwiatkowski\CatalogBanners\ViewModel;

use Magento\Catalog\Model\Layer\Resolver;
use Magento\Cms\Model\Template\FilterProvider;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Mkwiatkowski\CatalogBanners\Api\BannerRepositoryInterface;
use Mkwiatkowski\CatalogBanners\Api\Data\BannerInterface;
use Psr\Log\LoggerInterface;

/**
 * Class CatalogBanners
 */
class CatalogBanners implements ArgumentInterface
{
    /**
     * @var BannerRepositoryInterface
     */
    private $bannerRepository;
    /**
     * @var SearchCriteriaBuilder
     */
    private $searchCriteriaBuilder;
    /**
     * @var LoggerInterface
     */
    private $logger;
    /**
     * @var Resolver
     */
    private $resolver;
    /**
     * @var FilterProvider
     */
    private $filterProvider;

    /**
     * CatalogBanners constructor.
     * @param BannerRepositoryInterface $bannerRepository
     * @param SearchCriteriaBuilder $searchCriteriaBuilder
     * @param LoggerInterface $logger
     * @param Resolver $resolver
     * @param FilterProvider $filterProvider
     */
    public function __construct(
        BannerRepositoryInterface $bannerRepository,
        SearchCriteriaBuilder $searchCriteriaBuilder,
        LoggerInterface $logger,
        Resolver $resolver,
        FilterProvider $filterProvider
    ) {
        $this->bannerRepository = $bannerRepository;
        $this->searchCriteriaBuilder = $searchCriteriaBuilder;
        $this->logger = $logger;
        $this->resolver = $resolver;
        $this->filterProvider = $filterProvider;
    }

    /**
     * Get random banner for current category.
     *
     * @return string[]|null
     */
    public function getBannersForCurrentCategory() : ?array
    {
        $currentCategory = $this->resolver->get()->getCurrentCategory();
        $bannersOutput = [];

        $searchCriteria = $this->searchCriteriaBuilder
            ->addFilter(BannerInterface::CATEGORY_ID, $currentCategory->getId())
            ->addFilter(BannerInterface::IS_ACTIVE, BannerInterface::STATUS_ENABLED)
            ->create();

        try {
            $banners = $this->bannerRepository->getList($searchCriteria)->getItems();

            foreach ($banners as $banner) {
                $bannersOutput[] = $this->filterProvider->getBlockFilter()->filter($banner->getContent());
            }

            return $bannersOutput;
        } catch (\Exception $e) {
            $this->logger->notice($e->getMessage());

            return null;
        }
    }
}

Każdy ViewModel musi implementować interfejs Magento\Framework\View\Element\Block\ArgumentInterface

Jak widzisz klasa nie musi dziedziczyć po żadnej klasie, a zależności jakie są wstrzykiwane do konstruktora są tylko takie, jakie potrzebujemy. W klasie zdefiniowałem jedną metodę getBannersForCurrentCategory, której zadaniem jest pobranie contentu aktywnych banerów dla kategorii na której blok zostanie wyrenderowany.

ViewModel: wstrzyknięcie i obsługa w widoku (templatce)

Tworzę plik Mkwiatkowski/CatalogBanners/view/frontend/templates/category/banner.phtml (taką ścieżkę zdefiniowałem w layout XML dla bloku)

<?php
/** @var $block \Magento\Framework\View\Element\Template */
/** @var \Mkwiatkowski\CatalogBanners\ViewModel\CatalogBanners $viewModel */
$viewModel = $block->getViewModel();
$banners = $viewModel->getBannersForCurrentCategory();
?>
<?php if (null !== $banners): ?>
    <?php foreach ($banners as $bannerContent): ?>
        <div class="banner">
            <div class="banner__content">
                <?= $bannerContent; ?>
            </div>
        </div>
    <?php endforeach; ?>
<?php endif; ?>

W czwartek linii „wyciągam” VievModel i mogę pracować na tym obiekcie. Niżej wywołuję metodę getBannersForCurrentCategory i wyświetlam content bannerów.

Teraz gdy wejdę na stronę kategorii dla której mam zdefiniowane bannery i są one aktywne to poniżej listy produktów pokaże mi się blok z banerami.

ViewModel

Konfiguracja w panelu admina wygląda tak:

Lista banerów
Edycja baneru

Jeśli śledzisz moje artykuły z tej serii od początku to pewnie pamiętasz o założeniu, że banery mają się pokazywać po środku listy produktów. Tym aspektem będę się zajmował następnym razem. Kod, który wyprodukowałem na potrzeby tego posta znajdziesz na moim Githubie.

ViewModel: podsumowanie

Na koniec chciałem zwrócić uwagę na jeszcze jedną sprawę. Jeśli kiedykolwiek ktoś się Ciebie zapyta: Jakie są różnice pomiędzy Blockiem a ViewModelem? Takie pytanie może paść np. na rozmowie o prace albo na egzaminie Magento to odpowiedz:

ViewModel zawiera, logikę biznesową, ale do VIewModelu nie ma templatki, ani logiki związanej z wyświetlaniem/renderowaniem danych, natomiast Block moze zawierać logikę biznesową i logikę (templatkę) renderowania danych.

Źródła