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