Applicazione AJAX con Laravel

Spread the love
ajax logo
ajax logo

Premetto che, a quanto ho visto, non c’è un vero e proprio standard in Laravel per gestire chiamate asincrone (AJAX). Quindi il tutto rimane sempre un po’ sporco perché si è costretti a mescolare il codice generando, ad esempio, codice HTML al volo con Javascript. Non ho trovato un modo diverso.

Laravel ci viene incontro con le rotte, i controller, ma il momento topico di AJAX, cioè il momento in cui un singolo brano della pagina web viene aggiornato asincroncamente, è del tutto dirty.

Ma andiamo con ordine. Il mio esempio è una semplice form di ricerca in un tabella che visualizza i risultati mano a mano che si digita nella casella di testo.

Step 1: rotta con la quale invocare il server

La chiamata server to server è definita nel file routes/web.php oppure in route/api.php ed è la seguente

Route::post('companies/search', 'App\Http\Controllers\CompanyController@search')->name('companies.search');

Definisco cioè una nuova rotta che viene invocata con il metodo POST con la quale utilizzerò un nuovo metodo del controller.

Nota: il metodo POST implica che il payload (la stringa di ricerca) sia trasmesso al server nel corpo del messaggio, non nell’URL. Quindi non si deve aggiungere la stringa di ricerca nell’url:

Route::post('companies/search/{nome}', 'App\Http\Controllers\CompanyController@search')->name('companies.search');

Step 2: il controller

Il controller implementa un nuovo metodo, search(), che accetta un parametro di ingresso che è la Request che contiene la stringa che scriveremo nella casella di testo, esegue una query e ritorna il recordset con encoding JSON:

    public function search(Request $request)
    {
        $name = $request->all()['name'];
        $companies = Company::where('name', 'LIKE', '%'.$name.'%')->get();
        return json_encode($companies);
    }

So far so good.

Step 3: il controller per il Frontoffice

Ho creato un secondo controller che ha il ruolo di disegnatore della form del frontoffice: non è strettamente necessario ma ho in mente una serie di funzinalità che non sono soltanto inerenti al modello Company per cui astraggo da quest’ultimo.

<?php

namespace App\Http\Controllers;

use App\Models\Company;
use Illuminate\Http\Request;

class FrontofficeController extends Controller
{
    function index()
    {
        return view('frontoffice.index');
    }

}

Step 4: la view

@extends('layouts.app')

@section('content')
<div class="container">
    <form action="" method="post">
        <div class="form-group">
            <label for="name">Company</label>
            <input type="text" name="name" id="name" class="form-control" placeholder="Nome" autocomplete="off">
        </div>
        <div class="form-group">
            <button class="btn btn-success btn-submit">Invia</button>
        </div>
    </form>

    <div class="table">
        <table class="table" id="companies">
            <thead>
            <tr>
                <th scope="col">id</th>
                <th scope="col">name</th>
                <th scope="col">website</th>
                <th scope="col">email</th>
            </tr>
            </thead>
            <tbody>
            </tbody>
        </table>
    </div>
</div>
@endsection

Nota 1: La casella di testo che servirà da trigger per la chiamata AJAX si chiama #name.

Nota 2: c’è un blocco <tbody/> che andrà popolato asincronicamente

Step 5: jQuery does the magic!

Questo metodo, anche se lo trovate in 10000 forum, ha una sua complessità che spiego un po’ alla volta.

L’ho implementato dentro al file <APP>/resources/layouts/app.blade.php, anche se potrei isolarlo in un file javascript a parte e includerlo nel blocco HTML di head, tra i tag di <script>:

       $(document).ready(function() {
            $.ajaxSetup({
                headers: {
                    'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
                }
            });

            $("#name").keyup(function (e) {
                e.preventDefault();

                let name = $("input[name=name]").val();
                let CSRF_TOKEN = '{{ csrf_token() }}';

                if (name != '') {
                    $.ajax({
                        type: 'POST',
                        url: "{{ route('companies.search') }}",
                        data: {name: name, _token: CSRF_TOKEN},
                        success: function (data) {
                            let result_tag = "";
                            $.each(JSON.parse(data), function (i, item) {
                                result_tag += "<tr>" +
                                    "<td>" + item.id + "</td>" +
                                    "<td>" + item.name + "</td>" +
                                    "<td>" + item.website + "</td>" +
                                    "<td>" + item.email + "</td>" +
                                    "</tr>";
                            });
                            $('#companies tbody').html(result_tag);
                        }
                    });

                } else {
                    $('#companies tbody').html('');
                }
            });
        });

L’evento che scatena il Javascript è keyup, quando rilascio il tasto parte la chiamata asincrona:

Leggo il valore della casella di testo (linea 11); se nella variabile name c’è qualcosa, preparo la ricerca AJAX (linea 15), predisponendo il metodo POST (linea 16), l’url che ho definito nella rotta (linea 17) ed il passaggio dati nel corpo del messaggio HTTP (linea 18).

Se il servizio web risponde con un 200 (linea 19) creo i tag HTML di riga contenenti i risultati provenienti in JSON dal servizio web (il controller infatti li codifica in JSON) . Terminato il ciclo metto tutto il contenuto all’interno dei tag <tbody> (linea 29).

Nota che il tbody viene riscritto ad ogni pressione del tasto sulla tastiera.

Quest’ultimo Step è un po’ messy, si mescolano in un solo file HTML, Blade (e fin qui OK) ma anche Javascript. È una soluzione sporca, che non mi piace. Il problema è che qui affido la generazione dell’HTML ad un programma Javascript anziché a Blade. In sostanza però la logica di generazione del’HTML è in un file blade, quindi a rigore non starei derogando del tutto dal paradigma MVC.

Ultimo step: il csfr_token

Eh, se avete letto con attenzione fin qui, vi sarete chiesti come mai sorvolavo sempre su questo aspetto. Era importante dedicargli una sezione a parte.

Come detto in quest’altro articolo, è importante verificare che l’interrogzione del webservice search sia fatta dal sito stesso e non da qualcun altro, per evitare i problemi anche gravi conseguenti alla non adozione di contromisure.

La tecnica è quella di aggiungere al form html il tag @csfr che fa inviare al server una stringa complessa che poi deve venire controllata lato server. Siccome solo il server conosce questa stringa ci siamo protetti dagli abusi.

Il tag @csfr si può anche definire in un tag meta dellHTML e leggerlo poi in sede di servizo web confrontandolo con quello che ci arriva.

In realtà dobbiamo solo preoccuparci della trasmissione del token in quanto già Laravel controlla per noi la congurenza: se non arriva il token arriva con un valore sbagiato il server Apache emetterà un errore 419 e non funzionerà i servizio.

Il token può essere generato con la direttva @csfr scritta nel Form html , oppure con la funzione di blade {{csrf_token()}}, oppure inserito in un tag meta dell’intestazione html:

<!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

per poi essere letta dal modulo javascript:

$('meta[name="csrf-token"]').attr('content')

Visto che è la stessa pagina posso anche calcolarlo direttamente (come ho fatto in linea 12) per poi essere spedito in più modi al server; o nel payload della richesta (linea 18) oppure nell’intestazione HTTP (linee 2-6). O in entrambe.

L’omissione di questo passaggio provoca un errore 419 (unknown status).

È tutto.

Riferimenti in rete

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.