Magento 2 wykorzystuje powszechnie znany wzorzec projektowy jakim jest Repozytorium (Repository pattern). Repozytorium ma zapewnić metody dostępu do konkretnych danych i ważne tutaj jest to, że logika biznesowa nie może mieć pojęcia jak te dane są przechowywane. Można powiedzieć, że repozytorium stoi między warstwą danych (np. baza danych, pliki, zewnętrzna usługa), a logiką biznesową.

Zachęcam do przeczytania tych artykułów, jeśli wzorzec repozytorium i jego wykorzystanie w M2 nie jest Ci dobrze znany:

Implementacja Repozytorium danych

Możemy zabrać się za zaimplementowanie repository na potrzeby naszego modułu CatalogBanners. Ten artykuł to czwarta część praktycznego poradnika dla Magento developerów. Tutaj znajdziesz wstęp, z którego dowiesz się, o czym tak naprawdę jest ten poradnik.

Pierwszym krokiem jaki zrobimy jest utworzenie interfejsu dla naszego repozytorium.

app/code/Mkwiatkowski/CatalogBanners/Api/BannerRepositoryInterface.php

interface BannerRepositoryInterface
{
    /**
     * Save banner.
     *
     * @param \Mkwiatkowski\CatalogBanners\Api\Data\BannerInterface $banner
     * @return \Mkwiatkowski\CatalogBanners\Api\Data\BannerInterface
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function save(\Mkwiatkowski\CatalogBanners\Api\Data\BannerInterface $banner);

    /**
     * Retrieve banner
     *
     * @param int $bannerId
     * @return \Mkwiatkowski\CatalogBanners\Api\Data\BannerInterface
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function getById(int $bannerId);

    /**
     * Retrieve banners matching the specified criteria.
     *
     * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
     * @return \Mkwiatkowski\CatalogBanners\Api\Data\BannerSearchResultsInterface
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria);

    /**
     * Delete banner.
     *
     * @param \Mkwiatkowski\CatalogBanners\Api\Data\BannerInterface $banner
     * @return bool true on success
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function delete(\Mkwiatkowski\CatalogBanners\Api\Data\BannerInterface $banner);

    /**
     * Delete banner by id.
     *
     * @param int $bannerId
     * @return bool
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function deleteById(int $bannerId);
}

Dlaczego w docblocku używam pełnej ścieżki do klasy?

Teoretycznie zamiast pisać tak:

/**
     * Retrieve banners matching the specified criteria.
     *
     * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
     * @return \Mkwiatkowski\CatalogBanners\Api\Data\BannerSearchResultsInterface
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria);

Mógłbym zadeklarować tą metodę tak:

/**
     * Retrieve banners matching the specified criteria.
     *
     * @param SearchCriteriaInterface $searchCriteria
     * @return BannerSearchResultsInterface
     * @throws LocalizedException
     */
    public function getList(SearchCriteriaInterface $searchCriteria) : BannerSearchResultsInterface;

Oczywiście w tym przypadku wszystkie klasy, których używam są zaimportowane poprzez use. Niestety w Magento nie można tak robić, ponieważ repozytoria mogą być używane m. in. przez REST API, a tam parametry jakie są przekazywane w request, oraz to co ma być zwrócone w odpowiedzi jest określane na podstawie docblocka… W tym przykładzie API zwróci błąd class BannerSearchResultsInterface does not exist.

Co to jest BannerSearchResultsInterface?

Dokumentacja Magento mówi, że metoda getList(SearchCriteria $searchCriteria) powinna zwracać Search Reulst object, który jest instacją klasy, która powinna implementować interfejs SearchResultInterface. Mój BannerSearchResultsInterface rozszerza wlaśnie ten interfejs.

interface BannerSearchResultsInterface extends SearchResultsInterface
{
    /**
     * Get items.
     *
     * @return \Mkwiatkowski\CatalogBanners\Api\Data\BannerInterface[]
     */
    public function getItems();

    /**
     * Set items.
     *
     * @param \Mkwiatkowski\CatalogBanners\Api\Data\BannerInterface[] $items
     * @return \Mkwiatkowski\CatalogBanners\Api\Data\BannerSearchResultsInterface
     */
    public function setItems(array $items);
}

Jak widzisz rozszerzenie interfejsu SearchResultsInterface tak naprawdę sprowadza się do nadpisania deklaracji metod setItems i getItems. Zwróć uwagę na dockbloki.

Implementacja interfejsu BannerRepository

Klasę repozytorium tworzymy w folderze app/code/Mkwiatkowski/CatalogBanners/Model. Nazwę ją (uwaga niespodzianka) BannerRepository. Klasa oczywiście implementuje interfejs BannerRepositoryInterface.

<?php
namespace Mkwiatkowski\CatalogBanners\Model;

class BannerRepository implements BannerRepositoryInterface
{
}

Teraz zajmiemy się implementacją metod i sprawimy, że nasze repozytorium będzie roboło to czego od niego oczekujemy.

Metoda save

Metoda save otrzymuje BannerInterface jako parametr, zapisuje go i zwraca.

**
 * Save banner.
 *
 * @param BannerInterface $banner
 * @return BannerInterface
 * @throws CouldNotSaveException
 */
public function save(BannerInterface $banner)
{
    try {
        $this->resource->save($banner);
    } catch (\Exception $exception) {
        throw new CouldNotSaveException(__($exception->getMessage()));
    }

    return $banner;
}

Czym jest $resource?

Jest to Resource model naszych bannerów, czyli klasa Mkwiatkowski\CatalogBanners\Model\ResourceModel\Banner. Należy ją wstrzyknąć w konstruktorze i przypisać do pola $this->resource

W przypadku gdy zapis się nie uda rzucany jest wyjątek CouldNotSaveException.

Metoda getById

Metoda getById przyjmuje jako parametr id banneru i zwraca banner o takim id, jeśli istnieje.

/**
 * Retrieve banner.
 *
 * @param int $bannerId
 * @return BannerInterface
 * @throws NoSuchEntityException
 */
public function getById(int $bannerId)
{
    /** @var BannerInterface $banner */
    $banner = $this->bannerFactory->create();
    $this->resource->load($banner, $bannerId);
    if (!$banner->getId()) {
        throw new NoSuchEntityException(__('The banner with the "%1" ID doesn\'t exist.', $bannerId));
    }
    
    return $banner;
}

BannerFactory to fabryka naszych bannerów, czyli klasa Mkwiatkowski\CatalogBanners\Api\Data\BannerInterfaceFactory. Za pomocą metody getById można pobrać banner, a jeśli banner o określonym ID nie istnieje to metoda rzuca wyjątek NoSuchEntityException.

Metoda getList

Funkcja ta przyjmuje Search Criteria jako parametr i zwraca nasz Search Results Object.

    **
     * Retrieve banners matching the specified criteria.
     *
     * @param SearchCriteriaInterface $searchCriteria
     * @return BannerSearchResultsInterface
     * @throws LocalizedException
     */
    public function getList(SearchCriteriaInterface $searchCriteria)
    {
        $collection = $this->collectionFactory->create();
        $searchResults = $this->searchResultsFactory->create();

        $searchResults->setSearchCriteria($searchCriteria);
        $this->collectionProcessor->process($searchCriteria, $collection);
        $searchResults->setItems($collection->getItems());
        $searchResults->setTotalCount($collection->getSize());

        return $searchResults;
    }

Co to jest $this->collectionFactory?

Jest to fabryka naszych bannerów czyli klasa Mkwiatkowski\CatalogBanners\Model\ResourceModel\Banner\CollectionFactory

Co to jest $this->searchResultsFactory?

Jest to fabryka naszego Search Results object, czyli Mkwiatkowski\CatalogBanners\Api\Data\BannerSearchResultsInterfaceFactory

Co to jest $this->collectionProcessor?

Collection processor to obiekt, który implementuje interfejs \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface. Jest to miejsce gdzie można aplikować customowe filtry, sortowanie, paginację. Interfejs zawiera jedną metodę process, która aplikuje Search Criteria na kolekcję.

Moteda delete

Przy użyciu tej metody można usunąć banner. Jako parametr metoda dostaje BanneInterface Gdy usunięcie jest z jakiegoś powodu niemożliwe to metoda rzuca wyjątek CouldNotDeleteException.

    /**
     * Delete banner.
     *
     * @param BannerInterface $banner
     * @return bool
     * @throws CouldNotDeleteException
     */
    public function delete(BannerInterface $banner)
    {
        try {
            $this->resource->delete($banner);
        } catch (\Exception $exception) {
            throw new CouldNotDeleteException(__($exception->getMessage()));
        }

        return true;
    }

Metoda deleteById

Dzięki tej metodzie możemy usunąć banner. Różni się ona od metody delete tym, że jako parametr przyjmuje ID Banneru zamiast BannerInterface.

    /**
     * Delete banner by id.
     *
     * @param int $bannerId
     * @return bool
     * @throws CouldNotDeleteException
     * @throws NoSuchEntityException
     */
    public function deleteById(int $bannerId)
    {
        return $this->delete($this->getById($bannerId));
    }

Czy to wszystko?

Zrobiliśmy interfejs i jego implementację. Czy nasze repozytorium danych jest gotowe?

Nie! Musimy jeszcze dodać preference dla interfejsu.

app/code/Mkwiatkowski/CatalogBanners/etc/di.xml

<preference for="Mkwiatkowski\CatalogBanners\Api\BannerRepositoryInterface" type="Mkwiatkowski\CatalogBanners\Model\BannerRepository" />

Podsumowanie

Zrobiliśmy repozytorium danych dla modelu Bannerów,. Teraz przydałoby się to przetestować i właśnie to będziemy robić następnym razem! Cały kod z tego artykułu możesz znaleźć na moim Githubie.