Sprytna obsługa walidacji w Laravelu
Laravel to świetny framework PHP, który pomaga budować solidne aplikacje webowe i API. Jak pewnie wiesz istnieje wiele sposobów na przeprowadzenie walidacji żądania w Laravelu. Jest ona kluczowa w każdej aplikacji, a Laravel ma jeden sposób, który jest naprawdę fajny.
Zacznijmy
Pewnie wiesz jak użyć walidatora w kontrolerze. To najbardziej popularna metoda walidacji dla przychodzących żądań. Oto jak wygląda nasz walidator w UserController
:
<?php
namespace App\Http\Controllers\API\v1\Users;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use App\Entities\Models\User;
class UserController extends Controller
{
public function store(Request $request)
{
// validate incoming request
$validator = Validator::make($request->all(), [
'email' => 'required|email|unique:users',
'name' => 'required|string|max:50',
'password' => 'required'
]);
if ($validator->fails()) {
Session::flash('error', $validator->messages()->first());
return redirect()->back()->withInput();
}
// finally store our user
}
}
Nie ma nic złego w takim podejściu. Nie jest to jednak najlepszy sposób, a sam kod wygląda dość brzydko. Dlatego, to moim zdaniem zła praktyka. Kontroler powinien się zajmować jedynie obsłużeniem żądania na danej ścieżce i zwróceniem odpowiedniej odpowiedzi.
Dokładanie tutaj logiki dotyczącej walidacji łamie zasadę pojedynczej odpowiedzialności. Wiemy, że wymagania zmieniają się z czasem, przez co zmieniają się też odpowiedzialności klasy. Gdy pojedyncza klasa odpowiada za zbyt wiele, ciężko nią zarządzać.
Laravel ma tzw. Form Request
, osobną klasę żądania, zawierającą logikę walidacji. By stworzyć taką klasę, użyj komendy Artisana:
php artisan make:request UserStoreRequest
Utworzy ona nową klasę w ścieżce app\Http\Request\UserRequest
:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UserStoreRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'email' => 'required|email|unique:users',
'name' => 'required|string|max:50',
'password' => 'required'
];
}
/**
* Custom message for validation
*
* @return array
*/
public function messages()
{
return [
'email.required' => 'Email is required!',
'name.required' => 'Name is required!',
'password.required' => 'Password is required!'
];
}
}
Form Request ma w Laravelu dwie domyślne metody: auth()
i rules()
. W auth()
możesz wykonać dowolną logikę dotyczącą autoryzacji, by sprawdzić, czy użytkownik może wykonać dane żądanie. W rules()
możesz spisać wszystkie reguły walidacji. Jest też jedna dodatkowa metoda - messages()
, gdzie możesz przekazać własną tablicę z wiadomościami dotyczącymi walidacji.
Zmieńmy nasz UserController
by wykorzystywał UserStoreRequest
. Możesz użyć type-hintingu dla klasy żądania. Dzięki temu żądanie zostanie opakowane we właściwą klasę i zostaną wykonane walidacje, zanim odpali się główna logika kontrolera.
<?php
namespace App\Http\Controllers\API\v1\Users;
use App\Http\Controllers\Controller;
use App\Http\Requests\UserStoreRequest;
class UserController extends Controller
{
public function store(UserStoreRequest $request)
{
// Will return only validated data
$validated = $request->validated();
}
}
Więc nasz kontroler jest teraz znacznie bardziej zwięzły i łatwiejszy w utrzymaniu, bo nie musi się martwić logiką walidacji. Mamy naszą klasę walidacji, która przejęła tę odpowiedzialności od kontrolera.
Jeżeli walidacja się nie powiedzie, to przekieruje użytkownika do poprzedniej lokalizacji z błędem. W zależności od typu żądania, wiadomość ta zostanie zapisana jako wiadomość flash w sesji, a w przypadku AJAX zwróci odpowiedź z kodem 422 i odpowiedzią w formacie JSON.
Bonus
Czy pamiętasz o sprawdzaniu danych wejściowych? Sprawdzenie ich w aplikacji zapewni, że dane będą dobrze sformatowane i spójne. W wielu przypadkach walidacja zawodzi z powodu głupich błędów w formatowaniu.
Użytkownik może wprowadzić numer telefonu tak: +99 999999999, albo tak: +99 999 999 999. To częsty błąd, który może zmusić użytkownika, by wprowadzał jeszcze raz poprawne dane. Inne przykład mogą dotyczyć zamieniania małych i dużych liter w mailach czy imieniu i nazwisku.
Sanitizer zawiera metody, które pozwalają na transformowanie i filtrowanie danych do ustalonego formatu przed wystawieniem ich do walidatora.
Ja używam pakietu Waavi/Sanitizer
, który zawiera naprawdę sporo filtrów.
Stwórzmy abstrakcyjną klasę BaseFormRequest
dla naszego Form Request
i użyjmy cechy (po angielsku trait) SanitizesInput
.
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\JsonResponse;
use Waavi\Sanitizer\Laravel\SanitizesInput;
abstract class BaseFormRequest extends FormRequest
{
use SanitizesInput;
/**
* For more sanitizer rule check https://github.com/Waavi/Sanitizer
*/
public function validateResolved()
{
{
$this->sanitize();
parent::validateResolved();
}
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
abstract public function rules();
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
abstract public function authorize();
}
Teraz możemy zapisać nasz UserStoreRequest
, jak poniżej. Rozszerz swój From Request
z klasy bazowej, aby nie musieć dołączać cechy we wszystkich klasach.
<?php
namespace App\Http\Requests;
class UserStoreRequest extends BaseFormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'email' => 'required|email|unique:users',
'name' => 'required|string|max:50',
'password' => 'required'
];
}
public function messages()
{
return [
'email.required' => 'Email is required!',
'name.required' => 'Name is required!',
'password.required' => 'Password is required!'
];
}
/**
* Filters to be applied to the input.
*
* @return array
*/
public function filters()
{
return [
'email' => 'trim|lowercase',
'name' => 'trim|capitalize|escape'
];
}
}
Cecha SanitizeInput zapewnia metodę filters()
, do formatowania danych żądania, zanim przekażemy je do walidatora. filters()
zwraca tablicę filtrów. W powyższym przykładzie sprawiamy, że email będzie zapisany małymi literami i nie będzie zawierał zbędnych spacji.
Dla imienia, oprócz usunięcia zbędnych spacji, zapewnimy, że będzie się ono zaczynać wielką literą. Zakodujemy też tagi HTML.
Tu możesz poczytać więcej o dostępnych filtrach.
Podsumowanie
Na pierwszy rzut oka może się wydawać, że nie ma potrzeby robienia osobnej klasy dla żądania. Wyobraź sobie jednak wrzucenie wszystkich walidacji w tym samym kontrolerze. To będzie koszmar w utrzymaniu, nie tylko dla Ciebie, ale przede wszystkim dla innych.
Oryginał tekstu w języku angielskim możesz przeczytać tutaj.