DTO w formularzu Symfony – część 2

Chcę ponownie poruszyć temat używania DTO do obsługi formularzy w Symfony. Okazuje się, że można kilka spraw doprecyzować. Format wpisu będzie odrobinę inny. Postaram skupić się na konkretnych przypadkach użycia i je opisać.
Ten wpis rozszerza informacje zawarte w jednym z poprzednich wpisów: DTO w formularzu Symfony.
To null or not to null
DTO to nie jest Value Object. Podlega modyfikacjom. Jest workiem na dane. Dlatego zamiast getterów i setterów, udostępniam w nim publiczne atrybuty. Tylko dlaczego wszystkie są inicjowane z wartością null
? I dlaczego tak jest, nawet jeżeli jakieś dane są wymagane? Po pierwsze: kiedy to możliwe, staram się nie używać konstruktorów w DTO. To ma być pusty obiekt, który dopiero wypełnię danymi. Po drugie: DTO podlega zewnętrznej walidacji. Fakt, że korzystam z komponentu symfony/validator
chyba ma największy wpływ na „nulle”. Asercje definiują, czy dana wartość ma być pusta, jakiego ma być typu, długości i co mi tam jeszcze przyjdzie do głowy. Gdy walidator rozpoczyna swoją pracę, stara się dotrzeć do wartości przez publiczne atrybuty czy gettery. Gdybym nie inicjował wartości atrybutów, kończyłbym z błędem:
PHP Warning: Uncaught Error: Typed property Foo::$foo must not be accessed before initialization in php shell code:1 Stack trace: #0 {main} thrown in php shell code on line 1
No dobra, ale co się stanie, jeżeli zapomnę przepuścić dane przez validator i przekaże do encji null
zamiast jakiejś wartości? Podstawową ochroną jest definicja typu atrybutu metody w klasie czy konstruktorze. Przekaż złe dane i dostaniesz TypeError
. Drugą linią ochrony może być dodatkowa walidacja danych z wykorzystaniem np.: beberlei/assert
.
public function setTitle(string $title): void
{
Assertion::notBlank($title);
$this->title = $title;
}
Dzięki temu, gdy przekażę do encji niepoprawne dane, dostanę wyjątkiem w twarz.
Rozdzielenie danych między tworzeniem a edycją
Zazwyczaj mam możliwość wykorzystania jednej klasy do tworzenia i edycji danych w encji. Jest łatwiej, dane wejściowe zawsze obsługuję za pomocą tej samej klasy. Jakiś czas korzystałem z opcji „1 DTO 1 akcja”, jednak w większości przypadków obie klasy wyglądały tak samo z wyjątkiem konstruktora, który jako argument brał encję i z niej były inicjowane dane. Dużo łatwiej jest wykorzystać mechanizm częściowej walidacji na podstawie grup (https://symfony.com/doc/current/form/validation_groups.html).
Zostaje jeszcze uproszenie tworzenia obiektu wypełnionego istniejącymi danymi. Named constuctors na ratunek! Zamiast przyjmować atrybuty w konstruktorze, tworzę statyczną metodę fromEntity
:
class Foo
{
/**
* @Assert\NotBlank()
*/
public ?string $foo = null;
public static function fromEntity(Entity $bar): self
{
$data = new self();
$data->foo = $bar->getFoo();
return $data;
}
}
Prawda, że łatwiej?
Relacje między encjami
I znów problem, który rozwiązywałem na dwa sposoby.
Zacznijmy od prostszego w obsłudze. Można pomyśleć, że skoro do obsługi formularza wykorzystuje się DTO, to nie powinno się używać pola EntityType
. Tylko dlaczego nie? Przecież chodzi tylko o rozdzielenie danych przed walidacją od encji. EntityType
nie wprowadza zmian, a służy tylko do ich odczytu. Takie rozwiązanie ma jeszcze jeden plus: nie trzeba pisać odrębnego walidatora sprawdzającego, czy encja istnieje. Robi to za nas wbudowany mechanizm pola w formularzu. W takim przypadku obiekt danych powinien przyjąć encję, a nie inne DTO czy samo ID.
Teraz rozwiązanie odrobinę bardziej skomplikowane, w którym do DTO przekazuje się tylko ID encji. W takim przypadku, zamiast z EntityType
korzysta się z ChoiceType
. Teoretycznie, ChoiceType
załatwia za mnie sprawdzenie, czy wybrana opcja istnieje. Mimo wszystko wolę dopisać walidator, który upewni się, że encja o danym ID istnieje w bazie. Minusy tego rozwiązania to konieczność samodzielnego budowania listy opcji i walidatora.
Drugie rozwiązanie częściej sprawdza się w przypadku API – dlatego zaproponowałem przekazanie ID, a nie obiektu encji. Pracując z API i podstawowym serializerem Symfony, zazwyczaj otrzymujemy ID, a nie obiekt encji. Jeżeli dane pochodzą tylko z formularza, łatwiej jest zbudować listę opcji złożoną z encji, zamiast z ID. Mimo wszystko i tak dopisałbym walidator 🙂
Podsumowanie
Post powstał na skutek opublikowania postu w jednej z grup, do których należę na Facebooku. Zaczęło się od wyjątku spowodowanego błędnym typem danych przekazanych do settera.
Mam nadzieję, że ten artykuł rozwiał wątpliwości i pytania powstałe po przeczytaniu pierwszej części. Jeżeli masz jeszcze jakieś pytania lub sugestie, zapraszam do dodania komentarza.
Dodaj komentarz