- Published on
Praca z danymi w Laravel – od tablic do obiektów danych (DTO)
Wprowadzenie
W aplikacjach Laravel jednym z kluczowych elementów jest praca z danymi. Choć na pierwszy rzut oka może się to sprowadzać do operacji typu „zwróć tablicę” czy „zapisz rekord do bazy”, w większych projektach samo zarządzanie danymi staje się bardziej rozbudowane. Tradycyjne podejście oparte wyłącznie na modelach Eloquent i prostych tablicach może wtedy prowadzić do trudności w utrzymaniu i rozwoju aplikacji.
Pokażę jak wprowadzić do projektu bardziej przemyślane podejście do pracy z danymi, bazujące na obiektach danych (Data Transfer Objects, DTO). Dowiesz się, w jaki sposób silne typowanie oraz narzędzia statycznej analizy kodu mogą uprościć złożone operacje na danych, a także zobaczysz, jak przekuć dane wejściowe (np. dane z formularza) w ściśle zdefiniowane obiekty używane przez resztę aplikacji.
Typy i silne typowanie w PHP
Laravel – jako framework PHP – bazuje na języku, który jest dynamicznie i słabo typowany. Oznacza to między innymi, że:
- Słabe typowanie pozwala na przekazywanie wartości w różnych formatach (np.
int
może zostać przydzielony do zmiennej, która wcześniej byłastringiem
). - Dynamiczne typowanie oznacza, że weryfikacja typów odbywa się dopiero w czasie działania programu (runtime), a nie na etapie kompilacji (PHP nie jest kompilowane jak np. C++).
Choć te cechy PHP znacząco upraszczają pisanie prostych skryptów, w większych systemach mogą utrudnić kontrolę poprawności danych. W celu zwiększenia niezawodności warto stosować typowanie gdzie to możliwe czy zewnętrzne narzędzia do statycznej analizy (np. PHPStan, Psalm). Wszystko po to, by móc wcześniej wykrywać błędy i zyskać pewność, że nasze dane są zgodne z oczekiwaniami.
Od tablic do obiektów danych (DTO)
Rozważmy typowy scenariusz w aplikacjach Laravel: odbieramy dane z formularza i przechowujemy je w tablicy:
public function store(ProfileRequest $request, Profile $profile)
{
$validated = $request->validated();
$profile->name = $validated['name'];
$profile->email = $validated['email'];
// ...
}
Tablica $validated
zawiera dane zweryfikowane pod kątem walidacji, jednak wciąż nie mamy gwarancji, że np. klucz email
zawsze tam będzie. Dodatkowo, gdy ktoś inny w zespole pracuje nad podobnym fragmentem kodu, musi sam odgadnąć, jakie dokładnie pola są dostępne w $validated
.
W większych projektach taki sposób przekazywania danych potrafi być mylący oraz utrudniać statyczną analizę kodu. Obiekty danych (Data Transfer Objects) rozwiązują ten problem. Zamiast surowej tablicy używamy klasy, która jawnie określa, jakie pola i typy mogą się w niej znaleźć.
Przykład klasy DTO
class ProfileData
{
public function __construct(
public string $name,
public string $email,
public Carbon $birth_date
) {}
}
Teraz, zamiast przekazywać tablicę, tworzymy instancję ProfileData
. Dzięki temu IDE i narzędzia do analizy statycznej wiedzą, że w $name
musi być string
, w $email
również string
, a w $birth_date
obiekt typu Carbon
. To znacznie zwiększa czytelność i bezpieczeństwo kodu.
Tworzenie obiektów danych z danych wejściowych
Gdy nasz kontroler otrzymuje dane (np. z requestu), musimy je w jakiś sposób „spiąć” z naszą klasą DTO. Istnieje kilka sposobów:
- Konstruktor: Możemy przekazać wartości bezpośrednio do konstruktora:
public function store(StoreProfileRequest $request)
{
$profileData = new ProfileData(
$request->input('name'),
$request->input('email'),
Carbon::parse($request->input('birth_date'))
);
// ...
}
- Rozpakowanie tablicy (array spreading):
public function store(StoreProfileRequest $request)
{
$profileData = new ProfileData(...$request->validated());
// ...
}
Tu jednak musimy uważać na konwersje do odpowiednich typów (np. Carbon
).
- Fabryka lub statyczna metoda pomocnicza: Możemy stworzyć metodę
fromRequest
w fabryce lub w samej klasie, która wie, jak zmapować dane requestu na właściwości:
class ProfileData
{
//...
public static function fromRequest(StoreProfileRequest $request): self
{
return new self(
$request->get('name'),
$request->get('email'),
Carbon::parse($request->get('birth_date'))
);
}
}
Dzięki temu możemy ponownie użyć logki mapowania w innym miejscu.
Laravel Data
W celu usprawnienia tworzenia i obsługi obiektów DTO można sięgnąć po paczkę Laravel Data (stworzoną przez niezawodne Spatie). Umożliwia ona m.in.:
- Automatyczną konwersję typów (np.
string
→Carbon
). - Walidację pól w obiekcie danych (również za pomocą atrybutów np.
#[Rule('email')]
). - Łatwe mapowanie danych z requestu do obiektów DTO i odwrotnie.
Przykład użycia:
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Attributes\Validation\Rule;
class ProfileData extends Data
{
public function __construct(
public string $name,
#[Rule('email')]
public string $email,
public Carbon $birth_date
) {}
}
Następnie w kontrolerze możemy po prostu:
public function update(ProfileData $data)
{
// $data->name, $data->email, $data->birth_date
// ...
}
Paczka Laravel Data zadba o to, by name
i email
zostały zwalidowane, a birth_date
skonwertowana do Carbon
. Dzięki temu znacznie ograniczamy ryzyko błędów w trakcie działania programu.
Dlaczego obiekty danych?
W dużych projektach praca z danymi często staje się wąskim gardłem. DTO pozwalają:
- Zwiększyć czytelność: każdy w zespole od razu widzi, jak wygląda struktura danych (np. w IDE).
- Wymusić spójność: jasno określone typy zapobiegają błędom typu „tutaj oczekiwano daty, a trafił
string
”. - Poprawić testowalność: sam DTO można testować niezależnie od bazy danych.
- Zmniejszyć zależność od formatów danych: zmiana struktury wejścia (np. innych pól w request) nie rozbija nam logiki w całej aplikacji – wystarczy zaktualizować DTO (i ewentualnie fabrykę).
Podsumowanie
- DTO to skuteczny sposób na wprowadzenie typowanego, przejrzystego przepływu danych w aplikacji Laravel.
- Warto korzystać z narzędzi do analizy statycznej (PHPStan, Psalm) oraz wbudowanego typowania w PHP 7+, aby wcześniej wykrywać niezgodności w strukturze danych.
- Paczka Laravel Data dodatkowo automatyzuje konwersje, walidację i przekształcanie danych do obiektów, czyniąc rozwiązanie jeszcze wygodniejszym.
- Przejście z „samych tablic” na obiekty danych może wymagać pewnego nakładu pracy początkowej, ale w perspektywie dużych projektów szybko owocuje lepszą czytelnością i testowalnością kodu.
Dzięki tym technikom zyskujemy solidne fundamenty do implementacji pozostałych warstw aplikacji i łatwiej radzimy sobie z dalszym rozwojem projektu.