Magento 2: tworzenie modeli CRUD
W Magento 2 za obsługę operacji na danych używamy klas modeli, które łatwo pozwalają na zarządzanie danymi. Model obsługuje operacje CRUD (create — tworzenia, read — odczytu, update — aktualizacji i delete — usuwania). Dla przykładu stworzymy tabelkę przechowującą księgę gości odwiedzających stronę: guestbook.
Przyjrzyjmy się jakie modele są nam potrzebne do utworzenia i co Magento za nas generuje.
Utworzenie tabeli guestbook
Pierwszym krokiem będzie oczywiście utworzenie tabeli w bazie danych, którą następnie będziemy obsługiwać za pomocą modeli.
Ogólnie stworzyłam dwa wpisy na temat tego, jak możemy tego dokonać:
- Magento 2: tworzenie skryptów install i upgrade (wersja Magento < 2.3),
- Magento 2: tworzenie tabeli za pomocą db_schema.xml (używane od Magento 2.3).
Tworzenie modelu
Na początek stwórzmy interfejs, który będzie zawierał gettery i settery dla ustawiania/pobierania wartości kolumn z tabeli guestbook. Stworzona we wpisie tabela guestbook zawiera następujące kolumny:
- entry_id,
- customer_id,
- user_name,
- subject,
- content,
- created_at,
- visible.
Warto zawsze zajrzeć do jakiegoś magentowego modułu i zobaczyć jak pewne rzeczy są tam definiowane. Dla przykładu polecam klasę modelu Magento\Cms\Model\Block jak wygląda taka implementacja. Model ten implementuje interfejs Magento\Cms\Api\Data\BlockInterface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
<?php /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Cms\Api\Data; /** * CMS block interface. * @api * @since 100.0.2 */ interface BlockInterface { /**#@+ * Constants for keys of data array. Identical to the name of the getter in snake case */ const BLOCK_ID = 'block_id'; const IDENTIFIER = 'identifier'; const TITLE = 'title'; const CONTENT = 'content'; const CREATION_TIME = 'creation_time'; const UPDATE_TIME = 'update_time'; const IS_ACTIVE = 'is_active'; /**#@-*/ /** * Get ID * * @return int|null */ public function getId(); /** * Get identifier * * @return string */ public function getIdentifier(); /** * Get title * * @return string|null */ public function getTitle(); /** * Get content * * @return string|null */ public function getContent(); /** * Get creation time * * @return string|null */ public function getCreationTime(); /** * Get update time * * @return string|null */ public function getUpdateTime(); /** * Is active * * @return bool|null */ public function isActive(); /** * Set ID * * @param int $id * @return BlockInterface */ public function setId($id); /** * Set identifier * * @param string $identifier * @return BlockInterface */ public function setIdentifier($identifier); /** * Set title * * @param string $title * @return BlockInterface */ public function setTitle($title); /** * Set content * * @param string $content * @return BlockInterface */ public function setContent($content); /** * Set creation time * * @param string $creationTime * @return BlockInterface */ public function setCreationTime($creationTime); /** * Set update time * * @param string $updateTime * @return BlockInterface */ public function setUpdateTime($updateTime); /** * Set is active * * @param bool|int $isActive * @return BlockInterface */ public function setIsActive($isActive); } |
Zróbmy to samo. Najpierw utwórzmy katalog Anna/Guestbook/Api/Data i dodajmy do niego interfejs o nazwie GuestbookInterface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
<?php declare(strict_types=1); namespace Anna\Guestbook\Api\Data; interface GuestbookInterface { const ID = 'entry_id'; const CUSTOMER_ID = 'customer_id'; const USER_NAME = 'user_name'; const SUBJECT = 'subject'; const CONTENT = 'content'; const CREATED_AT = 'created_at'; const VISIBLE = 'visible'; const CACHE_TAG = 'anna_guestbook'; /** * Get entry id * * @return int|null */ public function getId(): ?int; /** * Set entry id * * @param int $entryId * @return GuestbookInterface */ public function setId($entryId); /** * Get customer id * * @return int|null */ public function getCustomerId(): ?int; /** * @param int|null $customerId */ public function setCustomerId(?int $customerId): void; /** * @return string */ public function getUserName(): string; /** * @param string $userName */ public function setUserName(string $userName): void; /** * Get subject * * @return string */ public function getSubject(): string; /** * Set subject * * @param string $subject */ public function setSubject(string $subject): void; /** * Get content * * @return string */ public function getContent(): string; /** * Set content * * @param string $content */ public function setContent(string $content): void; /** * Get created at * * @return string */ public function getCreatedAt(): string; /** * Set created at * * @param string $createdAt */ public function setCreatedAt(string $createdAt): void; /** * Get is visible * * @return bool */ public function getVisible(): bool; /** * Set is visible * * @param bool $visible */ public function setVisible(bool $visible): void; } |
Teraz stwórzmy klasę modelu Guestbook rozszerzającą klasę implementującą ten interfejs, w katalogu Anna/Guestbook/Model:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
<?php declare(strict_types=1); namespace Anna\Guestbook\Model; use Anna\Guestbook\Api\Data\GuestbookInterface; use Anna\Guestbook\Model\ResourceModel\Guestbook as GuestbookResource; use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Model\AbstractModel; class Guestbook extends AbstractModel implements IdentityInterface, GuestbookInterface { protected function _construct(): void { $this->_init(GuestbookResource::class); } /** * {@inheritdoc} */ public function getIdentities(): array { return [self::CACHE_TAG . '_' . $this->getId()]; } public function getId(): ?int { $id = $this->getData(self::ID); if (null !== $id) { $id = (int)$id; } return $id; } public function setId($entryId) { $this->setData(self::ID, $entryId); return $this; } /** * {@inheritdoc} */ public function getCustomerId(): ?int { $customerId = $this->getData(self::CUSTOMER_ID); if (null !== $customerId) { $customerId = (int)$customerId; } return $customerId; } /** * {@inheritdoc} */ public function setCustomerId(?int $customerId): void { $this->setData(self::CUSTOMER_ID, $customerId); } /** * {@inheritdoc} */ public function getUserName(): string { return $this->getData(self::USER_NAME); } /** * {@inheritdoc} */ public function setUserName(string $userName): void { $this->setData(self::USER_NAME, $userName); } /** * {@inheritdoc} */ public function getSubject(): string { return $this->getData(self::SUBJECT); } /** * {@inheritdoc} */ public function setSubject(string $subject): void { $this->setData(self::SUBJECT, $subject); } /** * {@inheritdoc} */ public function getContent(): string { return $this->getData(self::CONTENT); } /** * {@inheritdoc} */ public function setContent(string $content): void { $this->setData(self::CONTENT, $content); } /** * {@inheritdoc} */ public function getCreatedAt(): string { return $this->getData(self::CREATED_AT); } /** * {@inheritdoc} */ public function setCreatedAt(string $createdAt): void { $this->setData(self::CREATED_AT, $createdAt); } /** * {@inheritdoc} */ public function getVisible(): bool { return (bool)$this->getData(self::VISIBLE); } /** * {@inheritdoc} */ public function setVisible(bool $visible): void { $this->setData(self::VISIBLE, $visible); } } |
Każdy model musi implementować metodę _construct(). Metoda ta jest używana w klasie bazowej do wywołania metody _init(), która ustawia potrzebny resource model. Zadaniem resource modelu jest pobieranie informacji z bazy danych.
Dodatkowo implementujemy interfejs Magento\Framework\DataObject\IdentityInterface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Framework\DataObject; /** * Interface for * 1. models which require cache refresh when it is created/updated/deleted * 2. blocks which render this information to front-end */ interface IdentityInterface { /** * Return unique ID(s) for each object in system * * @return string[] */ public function getIdentities(); } |
W opisie mamy informację, dlaczego potrzebujemy go zaimplementować. W przypadku modeli jest on wymagany, jeśli potrzebujemy, aby był wymóg odświeżania cache’u po dokonywanych operacjach tworzenia, aktualizacji bądź usuwania informacji z bazy danych. Potrzebujemy również implementować to dla klas bloków, które wyświetlają informacje na frontendzie.
Tworzenie resource modelu
Zadaniem resource modelu jest pobierania danych z określonej tabeli w bazie danych. Każdy resource model musi dziedziczyć po klasie Magento\Framework\Model\ResourceModel\Db\AbstractDb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?php declare(strict_types=1); namespace Anna\Guestbook\Model\ResourceModel; use Anna\Guestbook\Api\Data\GuestbookInterface; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; class Guestbook extends AbstractDb { protected function _construct() { $this->_init( 'guestbook', GuestbookInterface::ID ); } } |
W metodzie _construct() wywołujemy metodę _init(), do której podajemy nazwę tabeli w bazie danych oraz nazwę klucza głównego.
Tworzenie modelu resource collection
W katalogu Anna\Guestbook\Model\ResourceModel\Guestbook tworzymy klasę Collection o następującej zawartości:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?php declare(strict_types=1); namespace Anna\Guestbook\Model\ResourceModel\Guestbook; use Anna\Guestbook\Model\Guestbook; use Anna\Guestbook\Model\ResourceModel\Guestbook as GuestbookResource; use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; class Collection extends AbstractCollection { protected function _construct(): void { $this->_init( Guestbook::class, GuestbookResource::class ); } } |
Klasa kolekcji musi dziedziczyć po klasie Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection.Używamy metody _construct() aby wywołać metodę _init(), której zadaniem jest zainicjalizowanie modelu i resource modelu.
Factory object
Czas teraz wyjaśnić właściwie, jak stworzone przez nas modele możemy użyć. Aby stworzyć obiekt danego modelu, używamy klasy fabryki. Nazwę klasy fabryki to nazwa modelu klasy z dodanym na końcu słowem 'Factory’. Więc w omawianym przeze mnie przykładzie, będzie to klasa GuestbookFactory. Tych klas nie tworzymy, Magento robi to za nas. Wygenerowane klasy fabryk odnajdziemy w katalogu var/generation (dla Magento 2.1.x i wcześniejszych wersji) albo /generated (dla Magento 2.2.x i wyżej). Warto usunąć zawartość katalogu, bądź też ustawić tryb developer poprzez komendę:
1 |
php bin/magento deploy:mode:set developer |
Warto również wyczyścić cache. Jeśli chodzi o tryb production powinniśmy użyć ten komendy:
1 |
php bin/magento setup:di:compile |
W ten sposób na nowo zostano wygenerowane pliki. Użycie klasy fabryki zaprezentuje na przykładzie klasy repozytorium.
Klasa repozytorium
Najpierw dodajmy interfejs, umieśćmy go w katalogu Anna/Guestbook/Api i stwórzmy interfejs o nazwie GuestbookRepositoryInterface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<?php declare(strict_types=1); namespace Anna\Guestbook\Api; use Anna\Guestbook\Api\Data\GuestbookInterface; use Anna\Guestbook\Api\Data\GuestbookSearchResultsInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Exception\CouldNotDeleteException; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\IntegrationException; use Magento\Framework\Exception\NoSuchEntityException; interface GuestbookRepositoryInterface { /** * @throws NoSuchEntityException */ public function getById(int $id): GuestbookInterface; /** * @throws IntegrationException */ public function getList(SearchCriteriaInterface $searchCriteria): GuestbookSearchResultsInterface; /** * @throws CouldNotSaveException */ public function save(GuestbookInterface $guestbook): void; /** * @throws CouldNotDeleteException */ public function delete(GuestbookInterface $guestbook): bool; /** * @throws CouldNotDeleteException */ public function deleteById(int $entryId): bool; } |
Implementacja klasy repozytorium Anna\Guestbook\Model\GuestbookRepository wygląda następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
<?php declare(strict_types=1); namespace Anna\Guestbook\Model; use Anna\Guestbook\Api\Data\GuestbookInterface; use Anna\Guestbook\Api\Data\GuestbookInterfaceFactory; use Anna\Guestbook\Api\Data\GuestbookSearchResultsInterface; use Anna\Guestbook\Api\Data\GuestbookSearchResultsInterfaceFactory; use Anna\Guestbook\Api\GuestbookRepositoryInterface; use Anna\Guestbook\Model\ResourceModel\Guestbook as GuestbookResource; use Anna\Guestbook\Model\ResourceModel\Guestbook\Collection as GuestbookCollection; use Anna\Guestbook\Model\ResourceModel\Guestbook\CollectionFactory as GuestbookCollectionFactory; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Exception\CouldNotDeleteException; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\IntegrationException; use Magento\Framework\Exception\NoSuchEntityException; class GuestbookRepository implements GuestbookRepositoryInterface { /** * @var GuestbookInterfaceFactory */ protected $guestbookFactory; /** * @var GuestbookSearchResultsInterfaceFactory */ protected $guestbookSearchResultsFactory; /** * @var GuestbookResource */ protected $guestbookResource; /** * @var GuestbookCollectionFactory */ protected $guestbookCollectionFactory; /** * @var CollectionProcessorInterface */ protected $collectionProcessor; public function __construct( GuestbookInterfaceFactory $guestbookFactory, GuestbookSearchResultsInterfaceFactory $guestbookSearchResultsFactory, GuestbookResource $guestbookResource, GuestbookCollectionFactory $guestbookCollectionFactory, CollectionProcessorInterface $collectionProcessor ) { $this->guestbookFactory = $guestbookFactory; $this->guestbookSearchResultsFactory = $guestbookSearchResultsFactory; $this->guestbookResource = $guestbookResource; $this->guestbookCollectionFactory = $guestbookCollectionFactory; $this->collectionProcessor = $collectionProcessor; } /** * @throws NoSuchEntityException */ public function getById(int $id): GuestbookInterface { $guestbook = $this->guestbookFactory->create(); $this->guestbookResource->load($guestbook, $id); if (!$guestbook->getId()) { throw NoSuchEntityException::singleField(GuestbookInterface::ID, $id); } return $guestbook; } /** * @throws IntegrationException */ public function getList(SearchCriteriaInterface $searchCriteria): GuestbookSearchResultsInterface { try { /** @var GuestbookCollection $collection */ $collection = $this->guestbookCollectionFactory->create(); $this->collectionProcessor->process($searchCriteria, $collection); /** @var GuestbookSearchResultsInterface $searchResults */ $searchResults = $this->guestbookSearchResultsFactory->create(); $searchResults->setItems($collection->getItems()); $searchResults->setSearchCriteria($searchCriteria); $searchResults->setTotalCount($collection->getSize()); } catch (\Throwable $e) { $message = __('An error occurred while getting guestbook list: %error', ['error' => $e->getMessage()]); throw new IntegrationException($message); } return $searchResults; } /** * @throws CouldNotSaveException */ public function save(GuestbookInterface $entry): void { try { $this->guestbookResource->save($entry); } catch (\Throwable $e) { $message = __('Could not save guestbook entry: %1', $e->getMessage()); throw new CouldNotSaveException($message, $e); } } /** * @throws CouldNotDeleteException */ public function delete(GuestbookInterface $entry): bool { try { $this->guestbookResource->delete($entry); } catch (\Exception $exception) { throw new CouldNotDeleteException(__($exception->getMessage())); } return true; } /** * @throws CouldNotDeleteException * @throws NoSuchEntityException */ public function deleteById(int $entryId): bool { return $this->delete($this->getById($entryId)); } } |
Przyjrzyjmy się metodzie getById(). Tworzymy w niej najpierw obiekt modelu za pomocą klasy fabryki, następnie za pomocą resource modelu wczytujemy do utworzonego obiektu dane z bazy danych. Sprawdzamy, czy udało się odczytać dane poprzez sprawdzenie id — w zależności od przypadku rzucamy wyjątek lub zwracamy załadowany obiekt modelu.
Pisząc w ten sposób korzystamy z zasady pojedynczej odpowiedzialności. Sam model posiada metodę load() ale jej użycie nie jest zalecane, w klasie Magento\Framework\Model\AbstractModel mamy o tym stosowną informację w komentarzu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/** * Load object data * * @param integer $modelId * @param null|string $field * @return $this * @deprecated 100.1.0 because entities must not be responsible for their own loading. * Service contracts should persist entities. Use resource model "load" or collections to implement * service contract model loading operations. */ public function load($modelId, $field = null) { $this->_getResource()->load($this, $modelId, $field); return $this; } |
Według praktyki Magento została również utworzona osobna klasa, która przechowuje wyniki pobranej kolekcji. Tak więc w kodzie klasy repozytorium GuestbookRepository mamy pewną klasę implementującą interfejs Anna\Guestbook\Api\Data\GuestbookSearchResultsInterface. Definicja interfejsu jest następująca:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php declare(strict_types=1); namespace Anna\Guestbook\Api\Data; use Magento\Framework\Api\SearchResultsInterface; interface GuestbookSearchResultsInterface extends SearchResultsInterface { /** * @return GuestbookInterface[] */ public function getItems(); /** * @param GuestbookInterface[] $items * @return $this */ public function setItems(array $items); } |
Klasa Anna\Guestbook\Model\GuestbookSearchResults implementująca powyższy interfejs:
1 2 3 4 5 6 7 8 9 10 11 12 |
<?php declare(strict_types=1); namespace Anna\Guestbook\Model; use Anna\Guestbook\Api\Data\GuestbookSearchResultsInterface; use Magento\Framework\Api\SearchResults; class GuestbookSearchResults extends SearchResults implements GuestbookSearchResultsInterface { } |
Kolejnym etapem będzie określenie konfiguracji, na podstawie której Magento 2 będzie nam inicjalizowało odpowiednie klasy do wykorzystywanych interfejsów.
Dodanie konfiguracji preference dla interfejsów
Przedstawiona klasa repozytorium GuestbookRepository korzysta z naszych klas poprzez interfejsy. Potrzebujemy więc określić w konfiguracji, jakie klasy mają być ładowane do konstruktora. Przekazanie odpowiedni zależności do klasy należy do obowiązków Object Managera, który na podstawie konfiguracji inicjalizuje odpowiednie klasy. Plik konfiguracyjny, który nas interesuje to di.xml, który jest definiowany w katalogu etc. Potrzebujemy po prostu powiedzieć Magento wprost jakie klasy chcemy ustawić:
1 2 3 4 5 6 7 |
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Anna\Guestbook\Api\Data\GuestbookInterface" type="Anna\Guestbook\Model\Guestbook" /> <preference for="Anna\Guestbook\Api\Data\GuestbookSearchResultsInterface" type="Anna\Guestbook\Model\GuestbookSearchResults" /> <preference for="Anna\Guestbook\Api\GuestbookRepositoryInterface" type="Anna\Guestbook\Model\GuestbookRepository" /> </config> |
Węzeł <preference> pozwala określić poprzez ustawienie swoich argumentów, dla jakiego interfejsu (for), jaką chcemy wybrać klasę implementująca go (type). Sam <preference> może również nam posłużyć do nadpisywania istniejących klas.
Magento 2: Tworzenie grida w adminie za pomocą komponentu UI - Web Porgramming
15 października 2020 @ 20:44
[…] Tworzenie Modeli CRUD, […]
Magento 2: Jak moduł oddziałuje z innymi modułami? - Web Programming
30 października 2023 @ 17:19
[…] mamy dwa moduły o nazwie Anna_GuestbookAdminUI oraz Anna_Guestbook. Żeby określić, że moduł Anna_GuestbookAdminUI jest zależny od modułu Anna_Guestbook należy […]
Magento 2: preference - Web Programming
3 listopada 2023 @ 22:19
[…] przykładu z modułu Anna_Guestbook mamy klasę repozytorium. Jeśli chcemy, aby Object Manager dla interfejsu […]
Magento 2 tryby działania aplikacji - Web Programming
20 grudnia 2023 @ 22:45
[…] kodu aplikacji (klas fabryk i […]
Magento 2: tworzenie kontrolera i menu w adminie - Web Programming
9 lutego 2024 @ 16:57
[…] Tworzenie tabel w bazie danych oraz modeli CRUD, […]
Magento 2: Object Manager - Web Programming
3 kwietnia 2024 @ 20:39
[…] wpisie związanym z tworzeniem modeli dla operacji CRUD, wygenerowana klasa fabryki dla modelu AnnaGuestbookModelResourceModelGuestbookCollection […]