Z kilku przyczyn delikatnie rozjechał mi się harmonogram cotygodniowy, ale czas
nadrobić straty. Poprzednio opisywałem doświadczenia zebrane podczas projektowania
procesu instalacji dla Codice. Zajmiemy się teraz kolejnym zagadnieniem,
które mimo iż oparte na przykładzie tego konkretnego projektu, jest jednym z
podstawowych zagadnień dla każdej aplikacji CRUD. Zachęcam do tego aby ten,
jak i kolejne wpisy traktować jako zwyczajne poradniki dla piszących w Laravelu,
z nutką historii w tle.
Za każdym razem gdy piszemy aplikację, której instancja ma być używana równolegle
przez więcej niż jedną osobę, pojawia się kwestia weryfikacji właściciela zasobu.
Prościej mówiąc - sprawdzenia czy obecnie zalogowany użytkownik ma uprawnienia
do wyświetlenia/edycji/usunięcia danego rekordu. Na pierwszy rzut oka sprawa
nie należy do skomplikowanych:
$note = Note::where('id', $request->get('id'))->where('user_id', Auth::id())->get();
Na przykładzie notatek, pobieramy rekord o ID przekazanym w adresie i zaznaczamy
że ID przypisanego użytkownika musi być równe identyfikatorowi obecnie zalogowanego
(metoda pomocnicza id()
z fasady Auth
). Przyjmijmy też że naszym celem jest
usunięcie wybranej notatki. W razie błędu przekierujemy zaś na inną stronę, na
której wyświetlimy czytelny dla użytkownika komunikat. Bardziej kompletny kod
oparty o metodzie przedstawionej powyżej mógłby wyglądać tak:
$note = Note::where('id', $request->get('id'))->where('user_id', Auth::id())->get();
if ($note === null) {
return redirect('/')->with('error', 'Nie jesteś właścicielem podanej notatki');
}
$note->delete();
return redirect('/')->with('success', 'Notatka usunięta');
W wypadku gdy nie uda się odnaleźć pasującego rekordu otrzymujemy null
. Wiemy
wtedy że podana notatka nie istnieje lub istnieje, ale należy do kogoś innego
(wymagamy spełnienia obu warunków), możemy więc cofnąć użytownika i wyświetlić
mu odpowiednią wiadomość.
Na dobrą sprawę wpis można by zakończyć już w tym miejscu. Warto jednak spojrzeć
bardziej perspektywicznie. Podany fragment kodu będzie się przecież powtarzał
także dla innych operacji CRUD (np. edycji lub odczytu), nasza aplikacja może
też zarządzać więcej niż jednym rodzajem danych (na przykładzie Codice - notatkami
oraz etykietami). W efekcie otrzymamy brzydką i niewygodną w utrzymaniu powtarzalność
kodu. Zastanówmy się jak można to poprawić.
Rozsądne wydaje się skorzystanie z metody find()
, która przyjmuje za konwencję,
że polem, po którym będziemy indeksować tabelę jest id
. Możemy połączyć to także
z możliwością opisaną w dokumentacji Laravela jako query scopes.
Do naszego modelu dodajemy:
public function scopeMine($query)
{
return $query->where('user_id', '=', Auth::id());
}
a wynikowo naszą metodę kontrolera redukujemy do postaci
$note = Note::mine()->find($request->get('id'));
if ($note === null) {
return redirect('/')->with('error', 'Nie jesteś właścicielem podanej notatki');
}
$note->delete();
return redirect('/')->with('success', 'Notatka usunięta');
Jest lepiej. Powtarzalna część zapytania została przeniesiona do modelu (oraz
konwencji używanej przez ORM Laravela). Wciąż jednak widzimy niezmienną część
logiki, którą także przydałoby się wypchnąć na zewnątrz, w jakieś wspólne miejsce.
W tym celu możemy wykorzystać wyjątki, bazując zresztą na logice używanej przez
wbudowaną metodę findOrFail()
. Do naszego modelu dodajemy kolejną metodę
pomocniczą:
public static function findMine($id)
{
$note = self::mine()->find($id);
if (!$note) {
throw new NoteNotFoundException;
}
return $note;
}
W obrębie aplikacji tworzymy wyjątek NoteNotFoundException
i importujemy
go w modelu.
<?php
namespace App\Exceptions;
class NoteNotFoundException extends \Exception {}
Logikę obsługi błędów przenosimy natomiast do handlera wyjątków, edytując
metodę render()
w klasie AppExceptionsHandler
.
public function render($request, Exception $e)
{
if ($e instanceof NoteNotFoundException) {
return Redirect::route('index')->with('message', 'Nie jesteś właścicielem podanej notatki`);
}
// reszta domyślnego kodu, np. ModelNotFoundException
}
Teraz kod użyty w kontrolerze powinien być naprawdę krótki i satysfakcjonujący:
$note = Note::findMine($request->get('id'));
$note->delete();
return redirect('/')->with('success', 'Notatka usunięta');
Jest dobrze? Tak. Może być lepiej? Oczywiście! Wady rozwiązania ujawnią się przy
jakiejkolwiek większej skali. Pozostając przy Codice - ten sam kod musiałby być
zduplikowany dla notatek i etykiet - dwa różne wyjątki, dwa bloki obsługujące go
w handlerze, analogiczne modyfikacje w obu modelach.
Skupimy się więc na wyłączeniu części wspólnych. Wyeliminujemy potrzebę tworzenia
indywidualnych wyjątków dla każdego rodzaju danych, a metody wymagane dla każdego
z modeli przeniesiemy do traita.
<?php
namespace Codice\Support\Traits;
use Auth;
use Illuminate\Http\Exceptions\HttpResponseException;
trait Owned
{
public static function findMine($id)
{
$model = self::mine()->find($id);
if (!$model) {
$modelLangFile = array_reverse(explode('\\', get_called_class()))[0];
throw new HttpResponseException(redirect()->route('index')->with([
'message' => trans("$modelLangFile.not-found"),
'message_type' => 'danger',
]));
}
return $model;
}
public function scopeMine($query)
{
return $query->where('user_id', '=', Auth::id());
}
public function user()
{
return $this->belongsTo('CodiceUser');
}
}
W tym wypadku obsługa po stronie kontrolera wygląda dokładnie tak, jak pokazano
wyżej, jednak cała implementacja logiki dla określonego modelu ogranicza się
do use Owned
, bez żadnego powtarzania schematycznego kodu. Dodatkowym klasom
wyjątków również możemy już podziękować, korzystamy z wbudowanego HttpResponseException
,
więc AppExceptionsHandler
może zostać przywrócone do wcześniejszej postaci.
Dodatkowo mamy też własny scope mine()
(przydatny przy innych rodzajach
zapytań niż wybranie pojedynczego rekordu po ID) oraz zdefiniowaną relację
do modelu użytkownika.
Ciekawostką jest hack w metodzie findMine()
, który pobiera nazwę modelu
wywołującego metodę i na tej podstawie buduje klucz do pliku z tłumaczeniami
(w celu zróżnicowania komunikatów błędów, np. "nie znaleziono notatki" vs
"nie znaleziono etykiety").
Przedstawiłem kilka rożnych metod, skracając nieco drogę, przez którą ten
kawałek kodu przeszedł w moim własnym projekcie. Zainteresowanych sprawdzeniem
faktycznego kodu mogę odesłać do historii zmian w
kontrolerze notatek oraz oczywiście traita Owned
Powyższy wpis przedstawia część doświadczeń zdobytych przy tworzeniu projektu
Codice. Po więcej szczegółów zapraszam do pierwszego wpisu oraz na
codice.eu. Poza planowaną od dawna serią jest to też realizacja
wymogów konkursu "Daj Się Poznać". Więcej o nim możecie przeczytać w
tym wpisie. Po nowe materiały poświęcone tworzeniu projektu Codice
zapraszam w każdy wtorek. Ponadto więcej wpisów, poświęconych temu projektowi
lub ogólnej tematyce bloga przeczytacie co sobotę.