KashYap Merai
KashYap MeraiTechnical Lead @ Confidential

Sprytna obsługa walidacji w Laravelu

Zobacz, jak wykorzystać Form Request do walidacji w Laravelu oraz jak używać pakietu Sanitizer do wstępnego przygotowania danych wejściowych.
29.12.20205 min
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.

<p>Loading...</p>