In questa lezione andremo ad approfondire come inviare dati alla nostra applicazione e come assicurarci che tali dati inviati siano corretti. Nelle applicazioni web, l’invio dei dati è abitualmente realizzato tramite form HTML: i dati inseriti dall’utente nel browser vengono inviati al server e memorizzati sul database.
Laravel rende estremamente semplice collegare i campi di un form HTML con una determinata tabella, seguendo una serie di accortezze legate alla implementazione del modello MVC (model/view/controller).
Particolarmente interessante e funzionale in Laravel è la validazione dei dati in ingresso verso la propria applicazione. Oltre, infatti, a un metodo validate che è possibile applicare a tutte le request che arrivano ai controller dell’applicazione, Laravel mette a disposizione molte regole di validazione che è possibile applicare a tali richieste: presenza di un campo, lunghezza, espressioni regolari.
Form e Input in Laravel
Laravel consente di creare con semplicità pagine e rotte che ci permettono di salvare a database i dati inseriti dall’utente nel browser. Per fare ciò, nel modo più semplice possibile andremo a riprendere quanto già introdotto nelle lezioni precedenti, in particolare Model, View e Controller, proseguendo nel nostro esempio di applicazione che gestisce libri e a autori.
Supponiamo, quindi, di avere disponibile il model App\Model\Book e la sua relativa tabella con migrazione. Per questo esempio considereremo che ogni libro deve essere caratterizzato da un titolo (con una lunghezza massima), un estratto (anch’esso con lunghezza massima, ma non richiesto) e il codice ISBN (che deve essere esattamente di 17 caratteri, deve essere presente e unico per ogni riga della tabella).
// in database/migrations/2023_01_01_create_books_table.php // ... public function up() { Schema::create('books', function (Blueprint $table) { $table->id(); $table->timestamps(); $table->string('title', 200); $table->string('excerpt', 500)->nullable(); $table->string('isbn', 17)->unique(); }); } // ... // app/Model/Book.php class Book extends Model { protected $fillable = [ 'title', 'isbn', 'excerpt' ]; }
Vogliamo permettere agli utenti della nostra applicazione di aggiungere un nuovo libro. Per fare ciò, creeremo una pagina con un form HTML per inserire i dati. Ci servirà, quindi, una view blade per il form HTML e una rotta per accedere a tale form. Per organizzare meglio il tutto, considereremo anche la presenza di un controller BookController.
// in routes/web.php Route::get('/books/create', [BookController::class, 'create']); Route::post('/books', [BookController::class, 'store']); // ... // app/Http/Controllers/BookController.php class BookController extends Controller { public function create() { return view('book.create'); } public function store() { // TODO } // ... } {{-- resources/views/book/create.blade.php --}} <div> <h1>Create a Book</h1> <form action="/books" method="POST"> @csrf <label for="title">Title</label> <input type="text" name="title" id="title"> <label for="isbn">ISBN</label> <input type="text" name="isbn" id="isbn"> <label for="excerpt">Excerpt</label> <input type="text" name="excerpt" id="excerpt"> <button type="submit">SUBMIT</button> </form> </div>
Qualche dettaglio e annotazione:
- aprendo la URI /books/create sulla nostra app verrà chiamato il metodo create del BookController
- tale metodo restituisce la view collegata
- nella view abbiamo inserito un form HTML con i vari input e il pulsante SUBMIT
- gli attributi HTML name e id dei singoli input hanno lo stesso nome delle colonne della tabella `books“
- nel momento in cui si fa clic sul pulsante del form, viene mandato il contenuto del form stesso tramite una richiesta POST all’endpoint /books
- abbiamo anche aggiunto la rotta che gestisce questa richiesta POST, ma non ne abbiamo ancora implementato il comportamento
- nel template blade è presente la direttiva @csrf, necessaria ad aggiungere un campo di sicurezza al form (senza di questo laravel rifiuterà la richiesta di invio dati come non valida)
- nel template blade abbiamo volutamente omesso di indicare quali degli input è required perché vogliamo che sia la nostra applicazione a gestire correttamente la validazione (che vedremo poco oltre nel dettaglio)
Ciò che vogliamo è che, una volta compilati gli input del form e fatto clic su SUBMIT, venga creata nel nostro database una nuova riga per un nuovo libro con i dati inseriti.
Dobbiamo, quindi, agire e implementare questo salvataggio nel metodo create del controller. All’interno di tale metodo, infatti, abbiamo a disposizione i dati inviati dalla richiesta POST, dovremo semplicemente farli salvare sul database.
// in app/Http/Controllers/BookController.php public function store(Request $request) { $attributes = $request->all() Book::create($attributes); }
Possiamo usare il metodo $request->all() per recuperare tutti gli input collegati alla request gestita e sfruttare il mass assignment del metodo create del Model per salvare a database. Ricordiamo, infatti, che abbiamo usato per i campi input del form gli stessi nomi e id delle colonne della tabella e la chiamata al metodo $request->all() restituirà qualcosa tipo il seguente array associativo.
[ '_token' => 'nAfaKAvaEbC6SExSzFSoFAp2JMu0l2hcX1jCj2nj', 'title' => 'A Book Title', 'isbn' => 'Some ISBN', 'excerpt' => 'Some excerpt from form input, with more details', ]
NOTA per maggiori dettagli sul funzionamento del metodo create di un Model e sulle implicazione di sicurezza di questo metodo fare riferimento alla lezione precedente sul mass assignment di Eloquent
Vale la pena notare che abbiamo implementato nel metodo store del controller solo il salvataggio a database tramite il model. Inviando il form, quindi, il nostro browser invierà una richiesta POST /books e farà il rendering della risposta che, in questo caso, è vuota (avremo, quindi, una pagina bianca). In un’ applicazione completa dovremmo gestire anche la risposta da inviare al browser oltre all’effettivo salvataggio dell’informazione.
In questo momento, però, ci interessa di più approfondire due aspetti legati alla correttezza del dato inviato. Il nostro codice, infatti, non sta applicando alcuna verifica sui dati inviati (title, isbn e excerp possono contenere qualsiasi valore scritto nel browser) e, quindi, l’unico check è effettuato lato database.
Validazione dell’input in Laravel
Laravel permette di convalidare i dati in ingresso alla propria applicazione in diversi modi. Il più comune è tramite il metodo validate che è possibile eseguire su ogni richiesta HTTP in arrivo.
Tramite tale metodo è possibile specificare una o più rule di validazione messe a disposizione da Laravel stesso. È, ovviamente, possibile creare delle proprie rule di validazione che si vanno ad aggiungere a quelle presenti di default nel framework.
Nel nostro caso d’esempio, sappiamo che le tre colonne sulla tabella hanno una lunghezza massima (cfr la migrazione riportata all’inizio). Possiamo, quindi, aggiungere delle regole di validazione che controllano, innanzitutto, la lunghezza dei vari campi inviati dal form.
// in app/Http/Controllers/BookController.php public function store(Request $request) { $attributes = request()->validate([ 'title' => 'max:200', 'isbn' => 'size:17', 'excerpt' => 'max:500' ]); Book::create($attributes); }
Il metodo validate opera sulla request e, in caso di successo, restituisce l’array degli input arrivati con la request. Nel caso in cui la richiesta sia valida, il codice continuerà a funzionare senza interruzioni. Le regole di validazione sono passate come argomento al metodo stesso sotto forma di array associativo, in cui la chiave è il nome del singolo input da validare e il valore è una delle regole di validazione.
Nel nostro caso d’esempio, abbiamo scelto le regole di validazione max e size che indicano la dimensione richiesta per ciascuno degli input: max:200 verificherà che la lunghezza dell’input non ecceda i 200 caratteri, size:17 invece che la lunghezza sia esattamente 17 caratteri.
Ci sono molte regole di validazione fornite direttamente da Laravel, per l’elenco completo rimandiamo alla documentazione ufficiale.
Volendo rendere la nostra validazione più efficace, dovremo, però, indicare più regole per alcuni input. Dalla definizione delle colonne della nostra tabella sappiamo che il campo title deve essere presente e che il campo isbn deve essere presente e deve essere unico per ogni riga del database. Possiamo, quindi, migliorare la nostra validazione nel modo seguente:
// in app/Http/Controllers/BookController.php public function store(Request $request) { $attributes = $request->validate([ 'title' => ['required', 'max:200'], 'isbn' => ['required', 'unique:books', 'size:17'], 'excerpt' => 'max:500' ]); Book::create($attributes); }
Possiamo usare le regole di validazione required e unique per indicare che:
- un input required deve essere presente e non può essere null o empty
- un input unique verifica che il valore passato non sia già presente nella tabella indicata (ovviamente con le usuali convenzioni di Laravel basate sulla corrispondenza per nome tra input e colonna da controllare)
Nel caso in cui la request non soddisfi le regole di validazione indicate, all’invio del form (POST /books) il metodo validate crea una opportuna risposta HTTP che fa redirect verso la precedente pagina (nel nostro esempio una 302 con header Location: /books/create).
Con tale redirect, Laravel ci offre la possibilità di mostrare quali errori di validazione si sono verificati per permettere all’utente di inserire dati corretti. Per fare ciò dobbiamo, però, brevemente introdurre i concetti di sessione e flash.
Sessioni e flash in Laravel
Sappiamo che il protocollo HTTP è un protocollo stateless, ossia che ogni richiesta è indipendente e senza memoria delle altre. Ciò non impedisce, però, che tra client e server sia possibile definire una determinata sessione sfruttando particolari meccanismi.
Nello specifico, un server è in grado di riconoscere le chiamate che provengono da uno specifico browser utilizzando i cookie. Alla prima richiesta effettuata dal browser, il server include nella risposta un cookie, creato a partire da una sessione creata per quel singolo browser e memorizzata sul server. Ad ogni richiesta successiva, il browser includerà tale cookie per indicare che la richiesta arriva da lui, permettendo, quindi, al server di sapere chi lo sta chiamando e come comportarsi. Su questa gestione dei cookie si basa ogni meccanismo che permetta di essere riconosciuti durante la navigazione su un sito, incluso, banalmente, il sapere che si è effettuato login.
Per il nostro caso sulla validazione di un input, quindi, l’applicazione Laravel è in grado di distinguere le richieste che provengono da uno stesso browser, poiché quel browser ha instaurato una sessione con l’applicazione stessa.
Un flash (o flash data) è un dato che viene salvato nella sessione a cui è associato e che sarà reso disponibile solo nella richiesta HTTP immediatamente successiva. È un meccanismo offerto da molti framework web, incluso ovviamente Laravel, pensato principalmente per far arrivare al browser messaggi di stato immediati, come ad esempio quelli degli errori di una validazione. Per il nostro caso sulla validazione di un input, è direttamente il metodo validate a creare il flash e ad aggiungerlo alla sessione. È comunque possibile aggiungere flash personalizzati: $request->session()->flash(‘status’, ‘Task was successful!’);.
Ricapitolando, nel nostro caso:
- un browser richiede per la prima volta la pagina con il form GET /books/create al nostro server
- il server crea una nuova sessione per quel browser, prepara la pagina HTML e restituisce al browser il contenuto della pagina e il cookie di sessione
- il browser renderizza la pagina con il form, l’utente riempie il form e fa clic sul pulsante
- il browser invia una richiesta POST /books con i dati inseriti dall’utente e con il cookie di sessione (per dire “sono sempre io”)
- il server riceve la richiesta, vede il cookie di sessione ricevuto, controlla se ci sono flash per quella sessione (nessuno per ora)
- effettua la validazione, trova un errore, aggiunge il flash alla sessione collegata, restituisce una redirect a `/books/create“
- a fronte della redirect il browser effettua una nuova richiesta a GET /books/create, sempre inviando il cookie di sessione
- il server elabora la richiesta, stavolta trova un flash associato, lo aggiunge alla richiesta
- il server prepara la pagina HTML aggiungendo i dati presenti nel flash durante tutte le fasi della elaborazione
Errori di validazione e ripopolazione del form in Laravel
Come possiamo, quindi, utilizzare sessioni e flash, nel nostro caso del form, per l’inserimento di un nuovo libro nel database? Tramite la variabile $errors disponibile in tutte le view del middleware web. Il meccanismo dei flash e la validate faranno in modo che in questa variabile siano presenti gli errori di validazione riscontrati.
Possiamo, quindi, modificare la nostra view nel modo seguente:
<div> <h1>Create a Book</h1> @if ($errors->any()) <div> <ul> @foreach ($errors->all() as $error) <li>{{ $error }}</li> @endforeach </ul> </div> @endif <form action="/books" method="POST"> {{-- non modificato --}} </form> </div>
In questo modo, in caso di errori, verrà mostrato l’elenco dei messaggi restituiti dal metodo validate (per esempio The title field is required oppure The isbn has already been taken). Sarà possibile gestire lato view come mostrare nello specifico tali errori.
Oltre agli errori di validazione, nel flash vengono salvati anche gli input forniti nel submit non riuscito. Tali input sono disponibili all’interno della request stessa tramite il metodo old e più semplicemente nei template Blade tramite l’helper old.
In questo modo, possiamo fare in modo che la nostra view faccia il pre-fill degli input già inseriti nel precedente invio non riuscito.
<label for="title">Title</label> <input type="text" name="title" id="title" value="{{ old('title') }}">
Classi form request in Laravel
Per scenari di validazione più complessi è possibile creare una classe dedicata alla validazione del form. Tali classi estendono la classe Illuminate\Foundation\Http\FormRequest ed implementano i metodi rules per le regole di validazione e authorize per determinare se la richiesta arriva da un utente autorizzato ad eseguirla.
Nel caso del nostro form, per aggiungere un libro avremmo, quindi, potuto creare una form request tramite l’opportuno comando Artisan
php artisan make:request StoreBookRequest
// app/Http/Requests/StoreBookRequest.php class StoreBookRequest extends FormRequest { public function authorize() { return true; } public function rules() { return [ 'title' => ['required', 'max:200'], 'isbn' => ['required', 'size:17', 'unique:books'], 'excerpt' => '' ]; } }
e regole di validazione possono essere dichiarate nello stesso modo in cui si passavano al metodo validate, ossia un array associativo con i nomi degli input da validare come chiavi e l’array delle regole attive per ogni input.
La nuova classe StoreBookRequest può essere utilizzata nel controller tramite dependency injection. In particolare, la sua istanza è considerabile come una normale istanza di Request, con la specificità di aver gestito autonomamente la validazione e di offrire metodi dedicati per sapere l’esito della validazione.
// app/Http/Controllers/BookController.php // ... public function store(StoreBookRequest $request) { // la richiesta in arrivo è valida, posso usarla ... $attributes = $request->all(); Book::create($attributes); // ma se servisse posso accedere al risultato della validazione $validated = $request->validated(); // ... }
Ogni altro comportamento descritto in precedenza resta, ovviamente, invariato. Il vantaggio di una FormRequest è ovviamente quello di tenere separate la definizione delle regole di validazione dal resto della gestione della request.