Klasy zestawów: jak wyklikać logikę?
W niniejszym tekście przekonamy się, jak Klasy Zestawów (Bundle Classes) pomogą nam lepiej zorganizować kod Drupala.
Podstawowym elementem pracy z Drupalem jest tworzenie Węzłów (Node). Masz zamiar zbudować stronę, więc instalujesz Drupala, ustawiasz zestawy węzłów (Node Bundles), a w nich pola dla tekstu, obrazów, plików, i czego tam jeszcze potrzeba. Na przykład Artykuł będzie mieć inne pola niż, dajmy na to, Portret: jeden będzie zawierał tekst, drugi zapewne obrazki i linki. I tak dalej.
Zestawy Węzłów możemy sobie szybko i sprawnie wyklikać. Nie musimy pisać kodu, możemy je tworzyć i definiować sposoby ich prezentacji przy pomocy narzędzi graficznych. Dzięki temu tworzenie podstawowych struktur danych w Drupalu jest szybkie i proste. Wszystko to możemy potem wyeksportować do plików konfiguracyjnych .yml.
Kod PHP potrzebny będzie do opisania logiki związanej z zachowaniem poszczególnych zestawów węzłów. Możesz na przykład chcieć, żeby przy przy ustawieniu jednego pola inne miało automatycznie wyliczaną wartość, ograniczyć dostęp do węzłów w zależności od wartości pól. Musimy zdecydować więc, gdzie będziemy umieszczać kod z tego rodzaju logiką.
Jednym z rozwiązań jest zrezygnowanie z Węzłów i stworzenie własnych Encji. Wszak zarówno węzły, jak i użytkownicy, to właśnie encje zdefiniowane przez konfiguracje i klasy PHP. W takich klasach możemy zawrzeć dowolną logikę. Oznacza to jednak, że zaczynamy budowanie naszej aplikacji od kodowania, a nie od klikania, co może (choć rzecz jasna wcale nie musi!) nie odpowiadać naszym wymogom.
Od niedawna w Drupalu dostępne jest bardzo ciekawe narzędzie: mamy bowiem możliwość definiowania klas PHP dla zestawów węzłów (oraz innych encji), jakie definiujemy przez interfejs użytkownika Drupala. Możemy więc modelować sobie na początek strukturę danych przez GUI, a następnie stworzyć klasy, w których zapiszemy sobie logikę zachowania naszych danych.
Jak mamy się do tego zabrać? Na początek instalujemy Drupala za pośrednictwem DDEV. Szczegółowe instrukcje znajdziemy tutaj – uzyskamy w ten sposób wygodny system kontenera do pracy z Drupalem do dalszej pracy. Przykładowa aplikacja omawiana w tym artykule dostępna jest w repozytorium Ratioweb https://github.com/RatioWeb/EntityStoreBlog/tree/main. DDEV oferuje gotowe do pracy narzędzie drush, zaś drush pozwala tworzyć klasy zestawów za pomocą prostych komend cli.
Podstawowa konfiguracja
Będziemy rozwijać przykładową aplikację o nazwie „Entity Store”. Mamy w niej Sklepy, Klientów i Partnerów (Stores, Customers, Partners): będą to nasze trzy zestawy węzłów. Zacznijmy od sklonowania repozytorium i uruchomienia ddev:
git clone git@github.com:RatioWeb/EntityStoreBlog.git cd entityStoresBlog git checkout init ddev start ddev composer install
Na końcu pliku `web/sites/default/settings.php` dodajemy linijkę (umiejscowienie plików konfiguracji):
$settings['config_sync_directory'] = $app_root .'/../config';
Zaczynamy od gałęzi init, gdzie będziemy mieli podstawą konfigurację drupala. Zaimportujmy teraz wyjściową bazę danych (komendy wydajemy w głównym katalogu aplikacji – domyślnie entityStoresBlog – powyżej folderu web z kodem drupala):
ddev import-db --file=init_entities.sql.gz dev drush cim -y
Ta ostatnia komenda zaimportuje konfigurację, ale nie powinna wprowadzać większych zmian – na poziomie init powinniśmy mieć te funkcjonalności, które są właśnie w wyjściowej bazie.
Możemy teraz zalogować się do strony przez
ddev drush uli
Dla uproszczenia mamy już zainstalowane moduły entity i devel_entity_updates. Jeśli chcielibyśmy wykonać to ręcznie, należałoby wydać polecenia:
composer require drupal/entity composer require drupal/devel_entity_updates drush en -y entity devel_entity_updates drush cex -y
Na tym etapie mamy to już zrobione. Baza danych zawiera encje EntityStore Customer, EntityStore Partner, EntityStore Store, a kod ma trzy własne moduły: entitystore_customer, entitystore_partner, entitystore_store, które znajdziemy w folderze web/modules/custom/. Na razie są one jeszcze puste, zawierają tylko zamarkowane pliki info.yml, .module, install i README.MD.
Jeśli chcesz tworzyć moduły samodzielnie, warto wiedzieć, że można w tym celu posłużyć się interaktywną komendą drush.
ddev drush generate module Welcome to module generator! –––––––––––––––––––––––––––––– Module name: ➤ EntityStore Store Module machine name [entitystore_store]: ➤ Module description: ➤ Functionality related to the stores. Package [Custom]: ➤ EntityStore Dependencies (comma separated): ➤ Would you like to create module file? [No]: ➤ yes Would you like to create install file? [No]: ➤ yes Would you like to create README.md file? [No]: ➤ yes The following directories and files have been created or updated: ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– • /var/www/html/web/modules/entitystore_store/entitystore_store.info.yml • /var/www/html/web/modules/entitystore_store/entitystore_store.install • /var/www/html/web/modules/entitystore_store/entitystore_store.module • /var/www/html/web/modules/entitystore_store/README.md
Następnie instalujemy je poleceniem
ddev drush en -y entitystore_customer,entitystore_partner,entitystore_store
Uzyskujemy w ten sposób bazową strukturę modułu. Przy uruchomieniu projektu drupala w ddev otrzymujemy automatycznie drush, więc nie musimy się martwić jego instalacją.
Ustawiania klas zestawów
Mamy więc jak na razie zestawy węzłów. W kodzie PHP naszej aplikacji instancje będą obiektami domyślnej klasy Node(web/core/modules/node/src/Entity/Node.php) Drupala, której nie powinno się modyfikować. My chcielibyśmy natomiast uzyskiwać własne klasy, które będziemy mogli wypełnić swoim kodem (częściowo oddzielnym dla każdej, częściowo wspólnym dla wszystkich).
Odpalamy kolejną komendę drush:
ddev drush generate entity:bundle Welcome to bundle-class generator! –––––––––––––––––––––––––––––––––––– Module machine name: ➤ entitystore_customer Entity type: [ 1] Content block [ 2] Comment [ 3] Contact message [ 4] File [ 5] Custom menu link [ 6] Content [ 7] URL alias [ 8] Shortcut link [ 9] Taxonomy term [10] User ➤ 6 Bundles, comma separated: [1] Article [2] EntityStore Customer [3] EntityStore Partner [4] EntityStore Store ➤ 3 Class for "EntityStore Partner" bundle [EntitystorePartner]: ➤ EntityStorePartner Use a base class? [No]: ➤ No The following directories and files have been created or updated: ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– • /var/www/html/web/modules/custom/entitystore_customer/entitystore_customer.module • /var/www/html/web/modules/custom/entitystore_customer/src/Entity/Node/EntityStorePartner.php
Powyższy przykład dotyczy zestawu węzłów EntityStore Partner, ale analogiczne komendy pozwolą nam stworzyć klasy dla pozostałych. Otrzymujemy więc trzy klasy:
web/modules/custom/entitystore_customer/src/Entity/Node/EntityStoreCustomer.php web/modules/custom/entitystore_partner/src/Entity/Node/EntityStorePartner.php web/modules/custom/entitystore_store/src/Entity/Node/EntityStoreStore.php
Oto zawartość jednej z nich:
<?php declare(strict_types = 1); namespace Drupal\entitystore_customer\Entity\Node; use Drupal\node\Entity\Node; /** * A bundle class for node entities. */ final class EntityStoreCustomer extends Node { }
Na razie nie mamy tu żadnego kodu, ale możemy wkrótce zakodować tu pożądaną funkcjonalność.
Ponadto drush tworzy też funkcje proceduralne w pliku .module:
function entitystore_customer_entity_bundle_info_alter(array &$bundles): void { if (isset($bundles['node']['entitystore_customer'])) { // phpcs:ignore Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing $bundles['node']['entitystore_customer']['class'] = EntityStoreCustomer::class; } }
Jeśli nie masz ochoty wpisywać tego kodu, wystarczy, że przeniesiesz się na gałąź git ‘feature/102-enitity-bundle-classes’: ten kod będzie już tam dostępny (zapewne warto wyczyścić cache).
Na tej gałęzi otrzymujemy też kontroler (wygenerowany przez `drush generate controller`) `web/modules/custom/entitystore_store/src/Controller/EntityStoreTestController.php`, który posłuży nam do testowania naszego kodu. Na tym etapie tworzy programowo encje i sprawdza, czy są one egzemplarzami naszych klas:
public function __invoke(): array { $partner = $this->entityTypeManager->getStorage('node')->create( [ 'status' => 1, 'type' => 'entitystore_partner', ] ); $customer = $this->entityTypeManager->getStorage('node')->create( [ 'status' => 1, 'type' => 'entitystore_customer', ] ); $store = $this->entityTypeManager->getStorage('node')->create( [ 'status' => 1, 'type' => 'entitystore_store', ] ); $is_custom_bundle_classes = is_a($partner, EntityStorePartner::class) && is_a($customer, EntityStoreCustomer::class) && is_a($store, EntityStoreStore::class); $result = $is_custom_bundle_classes ? $this->t('Test Passed') : $this->t('Test Failed'); $build['content'] = [ '#type' => 'item', '#markup' => $result, ]; return $build; } }
Przykład zastosowania: Blokada zapisu duplikatów
Enitty API Drupala oferuje narzędzie do tworzenia bezpiecznego duplikatu enecji. Służy do tego metoda EntityInterface::createDuplicate(). Klonuje ona nasz obiekt (polecenie clone() PHP), a na dodatek dba o to, by nowy obiekt był ustawiony jako nowy, miał własne UUID i tak dalej. Wyobraźmy sobie, że chcemy mieć gwarancję, że duplikaty naszych obiektów nie będą zapisane.
W tym celu dodajemy modyfikujemy klasę EntityStorePartner:
final class EntityStorePartner extends Node { protected bool $isDuplicate = FALSE; /** * {@inheritDoc} */ public function createDuplicate() { $duplicate = parent::createDuplicate(); $duplicate->setUnpublished(); $duplicate->setDuplicate(TRUE); return $duplicate; } /** * {@inheritDoc} */ public function save() { if (!$this->getIsDuplicate()) { return parent::save(); } else { return FALSE; } } /** * Checks whether the entity is a duplicate. */ public function getIsDuplicate():bool { return $this->isDuplicate; } /** * Flags as duplicate. * * @param bool $is_duplicate * * @return void */ protected function setDuplicate(bool $is_duplicate = TRUE) { $this->isDuplicate = $is_duplicate; } }
Tworzymy zmienną flagę instancji isDuplicate, która jest ustawiana przy wywoływaniu nadpisanej metody ::createDuplicate(). Nadpisujemy też metodę save(): jeśli flaga jest ustawiona, odmawiamy zapisu i zwracamy FALSE.
Kiedy encja entitystore_partner jest duplikowana createDuplicate(), nie da się jej zapisać do bazy.
Rozbudowujemy nasz testowy kontroler:
/** * Builds the response. */ public function __invoke(): array { $partner = $this->entityTypeManager->getStorage('node')->create( [ 'status' => 1, 'title' => 'A Partner example', 'type' => 'entitystore_partner', ] ); $customer = $this->entityTypeManager->getStorage('node')->create( [ 'status' => 1, 'title' => 'A customer example', 'type' => 'entitystore_customer', ] ); $store = $this->entityTypeManager->getStorage('node')->create( [ 'status' => 1, 'title' => 'A Store example', 'type' => 'entitystore_store', ] ); $is_custom_bundle_classes = is_a($partner, EntityStorePartner::class) && is_a($customer, EntityStoreCustomer::class) && is_a($store, EntityStoreStore::class); $messages[] = $is_custom_bundle_classes ? $this->t('Success: Custom bundle Classes creation test OK</br>') : $this->t('Fail: Custom bundle Classes creation test FAILURE</br>'); $partner_duplicate = $partner->createDuplicate(); $partner_save_result = $partner->save(); $partner_duplicate_save_result = $partner_duplicate->save(); $messages[] = $partner_save_result && !$partner_duplicate_save_result ? $this->t('Success: Duplicate save protection OK</br>') : $this->t('Failure: Duplicate save protection FAILURE </br>'); $result = ''; foreach ($messages as $message) { $result .= $message; } $build['content'] = [ '#type' => 'item', '#markup' => $result, ]; return $build; }
}
```
Tworzymy obiekty naszych własnych zestawów i sprawdzamy, czy korzystają z oddzielnych klas. Znów – po wejściu na https://entitystoresblog.ddev.site/entity_store_test powinniśmy uzyskać komunikat o poprawnej implementacji naszej funkcjonalności.
Oczywiście nie musimy tego wszystkiego robić ręcznie! Cały ten kod dostępny jest na gałęzi feature/104-duplicate-save-protection (trzeba tylko pamiętać o imporcie konfiguracji i wyczyszczeniu cache).
Podsumowanie
To tylko przykład tego, co można uzyskać dzięki klasom zestawów. Tak naprawdę nawet dysponowanie własnymi zmiennymi obiektowymi (polami) może być już bardzo przydatne. Wkrótce opiszemy kolejne możliwości i zastosowania tego narzędzia.
z ambitnymi ludźmi i projektami