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.

Komentarze (0)

Nikt jeszcze nie dodał komentarza.

Dodaj komentarz