Właściwość $casts w Laravelu to jedno z tych cichych udogodnień, które kształtują sposób obsługi danych, nie przyciągając przy tym uwagi. Wstaw do tablicy wbudowany typ, taki jak boolean czy datetime, a Eloquent automatycznie przekonwertuje surowe ciągi znaków z bazy danych na coś bardziej przyjaznego, zanim wartość trafi do Twoich kontrolerów lub widoków. Dzięki temu Twój kod pozostaje uporządkowany. Jednak w momencie, gdy spróbujesz wywołać własną funkcję pomocniczą (helper) bezpośrednio w tej tablicy, framework stawi opór. Zapisanie czegoś w stylu 'username' => 'encrypt_data()' nie zadziała. Laravel oczekuje albo natywnego słowa kluczowego rzutowania (cast), albo klasy implementującej interfejs CastsAttributes. Wymóg ten wynika z faktu, że warstwa rzutowania działa głęboko w cyklu hydratacji i serializacji Eloquent; potrzebuje ona przewidywalnego obiektu z jasnym kontraktem, a nie dowolnego ciągu znaków będącego nazwą funkcji, która może nie istnieć w czasie wykonywania programu (runtime).
Dlaczego funkcje pomocnicze nie działają w tablicy $casts
Gdy Eloquent ładuje model, skanuje tablicę $casts, aby zdecydować, jak przekształcić każdą kolumnę. Framework rozpoznaje konkretne typy prymitywne — string, integer, array, encrypted i tak dalej — oraz pełne nazwy klas (FQCN), które implementują CastsAttributes. Nie interpretuje on ciągu znaków jako kodu. Zatem 'encrypt_data()' jest traktowane jako nieznany typ rzutowania, co wywołuje błąd. Nawet gdyby Laravel go sparsował, nie byłoby niezawodnego sposobu na przekazanie instancji modelu, klucza atrybutu, aktualnej wartości oraz pozostałych atrybutów do Twojej funkcji w odpowiedniej kolejności. Interfejs istnieje właśnie po to, aby ustandaryzować to porozumienie między Twoją własną logiką a wewnętrznymi mechanizmami Eloquent.
Masz dwa sprawdzone sposoby na pokonanie tej bariery. Jeden sprzyja długofalowemu ponownemu wykorzystaniu kodu. Drugi stawia na szybkość, gdy potrzebujesz jedynie szybkiej poprawki w obrębie pojedynczego modelu.
Metoda 1: Napisz własną klasę rzutowania (Custom Cast Class)
Jeśli ta sama transformacja dotyczy wielu pól lub rozciąga się na kilka modeli, lepszym i czystszym rozwiązaniem jest dedykowana klasa rzutowania. Żyje ona w osobnym pliku, można ją testować jednostkowo w izolacji i dzięki niej Twoje modele pozostają wolne od powtarzalnego kodu (boilerplate).
Zacznij od utworzenia pliku app/Casts/CustomEncrypt.php. Przestrzeń nazw (namespace) powinna odpowiadać Twojej konfiguracji autoloadingu, zazwyczaj jest to App\Casts. Klasa musi implementować Illuminate\Contracts\Database\Eloquent\CastsAttributes, co wymusza zdefiniowanie dwóch metod: get oraz set.
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class CustomEncrypt implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
return decrypt_data($value);
}
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
{
return encrypt_data($value);
}
}
Sygnatury mają znaczenie. Eloquent przekazuje cztery argumenty do każdej metody. $model to instancja, która jest wypełniana lub zapisywana, co pozwala na sprawdzenie innych pól, jeśli Twoja logika od nich zależy. $key to nazwa kolumny, która jest aktualnie rzutowana. $value to surowy ciąg znaków lub null pobrany z bazy danych podczas get, lub wartość dostarczona przez użytkownika podczas set. $attributes to cała surowa tablica kolumn dla danego wiersza. Nie musisz używać wszystkich czterech argumentów za każdym razem, ale interfejs tego wymaga.
W powyższej metodzie get, decrypt_data($value) wykonuje się po tym, jak Eloquent pobierze wiersz z bazy danych, a przed tym, jak wartość trafi do Twojego modelu. W metodzie set, encrypt_data($value) wykonuje się przed operacją INSERT lub UPDATE, co gwarantuje, że baza danych nigdy nie zobaczy tekstu jawnego (plaintext).
Aby to połączyć, odwołaj się do klasy w tablicy $casts swojego modelu:
protected $casts = [
'username' => CustomEncrypt::class,
'password' => CustomEncrypt::class,
];
Ponieważ używasz stałej klasy, resztę załatwia autoloader. Jeśli Twoja aplikacja będzie później wymagała zmiany schematu szyfrowania, edytujesz tylko jeden plik, a zachowanie każdego zmapowanego pola zmieni się natychmiastowo. Taka centralizacja jest nie do pobicia, gdy zarządzasz wrażliwymi danymi w wielu tabelach.
Metoda 2: Skorzystaj z Accessora i Mutatora
Czasami nie chcesz tworzyć nowego pliku dla transformacji, która ma znaczenie tylko w jednym miejscu. Klasa Attribute w Laravelu pozwala zdefiniować logikę get i set bezpośrednio w modelu, korzystając ze składni closure dostępnej w PHP 8+.
use Illuminate\Database\Eloquent\Casts\Attribute;
protected function username(): Attribute
{
return Attribute::make(
get: fn ($value) => decrypt_data($value),
set: fn ($value) => encrypt_data($value),
);
}
Tutaj nazwa metody musi odpowiadać kolumnie lub atrybutowi, na którym Ci zależy. Gdy Eloquent odczytuje username z bazy danych, przekazuje surową wartość przez closure get. Gdy przypisujesz nową wartość właściwości username modelu, closure set szyfruje ją, zanim Eloquent zbuduje zapytanie.
Podejście to sprawdza się w prototypach lub modelach typu legacy, gdzie tworzenie całego katalogu klas rzutowania wydaje się przesadą. Wadą jest powtarzalność. Jeśli później uznasz, że email, phone i backup_code wymagają takiego samego traktowania, skończysz kopiując te closure do wielu metod lub modeli. Ten szum informacyjny narasta. Uniemożliwia to również ponowne wykorzystanie logiki w teście jednostkowym bez uruchamiania całego modelu.
Wybór między tymi dwiema opcjami
Użyj niestandardowej klasy rzutowania (custom cast class), gdy zależy Ci na ponownym wykorzystaniu, testowaniu i utrzymaniu modeli w lekkiej formie. Sygnalizuje to innym programistom, że ta transformacja jest kluczowym elementem Twojej aplikacji, a nie jednorazowym obejściem problemu.
Użyj akcesora (accessor), gdy logika jest ściśle lokalna, eksperymentalna lub mało prawdopodobne, że przetrwa obecny sprint. Pozwala to na szybkie działanie bez rozpraszania małych plików w całym kodzie źródłowym. Bądź jednak gotowy na refaktoryzację do klasy rzutowania, gdy ten sam wzorzec pojawi się po raz drugi lub trzeci.
Praktyczne szczegóły, o których warto pamiętać
Niestandardowe rzutowania są potężne, ale wprowadzają zachowania, które mogą Cię zaskoczyć, jeśli nie będziesz na nie uważać. Po pierwsze, pamiętaj, że metoda get otrzymuje wszystko, co zwróciła baza danych, w tym null. Jeśli decrypt_data nie obsługuje wartości null, zabezpiecz się przed tym:
public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
return is_null($value) ? null : decrypt_data($value);
}
Po drugie, rzutowania są wykonywane podczas serializacji do tablicy i formatu JSON. Gdy zwracasz model jako zasób API lub wywołujesz toArray(), logika get rzutowania nadal obowiązuje. Zazwyczaj jest to pożądane zachowanie, ale warto o tym pamiętać, jeśli udostępniasz odszyfrowane wartości i potrzebujesz nałożyć na nie dodatkowe mechanizmy kontroli widoczności.
Po trzecie, i co najważniejsze, rzutowanie następuje po pobraniu danych. Nie możesz wykonywać zapytań na przetransformowanej wartości. Zapytanie typu User::where('username', 'john_doe')->first() wysyła ciąg znaków john_doe bezpośrednio do bazy danych. Nigdy nie przechodzi ono przez Twoją logikę deszyfrowania. Jeśli kolumna jest zaszyfrowana w spoczynku (at rest), zapytanie to nie zwróci nic, chyba że będziesz przeszukiwać sam szyfrogram. Zaplanuj wzorce dostępu do bazy danych odpowiednio, ponieważ rzutowania nie są substytutem dla natywnych funkcji bazy danych czy indeksowanych kolumn tekstowych.
Niestandardowe klasy rzutowania stanowią również świetne miejsce na małe fragmenty konfiguracji. Jeśli potrzebujesz argumentów konstruktora — na przykład przekazania trybu szyfrowania lub ciągu formatującego — Laravel obsługuje je poprzez tablicę $casts, używając wyrażenia typu 'field' => CustomEncrypt::class . ':arg', choć wykracza to poza podstawową konfigurację opisaną tutaj.
Najważniejszy wniosek
Ograniczenie polegające na zakazie umieszczania funkcji pomocniczych bezpośrednio w $casts nie jest arbitralną biurokracją. Skłania ono do pisania kodu, który jest jawny, testowalny i możliwy do ponownego wykorzystania. Niestandardowe klasy rzutowania zamieniają rozproszoną logikę wewnątrz linii kodu w niezawodne komponenty, które możesz współdzielić między modelami bez duplikacji. Akcesory pozostawiają furtkę dla szybkich, lokalnych poprawek, gdy tworzenie osobnego pliku wydaje się zbędnym formalizmem. Opanuj oba podejścia, wybieraj w zależności od zakresu problemu, a Twoja warstwa Eloquent pozostanie czytelna długo po tym, jak aplikacja urośnie.
