Applicazione AJAX con Laravel

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

Laravel: i18n di una applicazione

i18n localizzazione di un'applicazione
i18n localizzazione di un’applicazione

La localizzazione (o internationalization o i18n) di Laravel è già gestita da un apposito middleware. Seguiamo questi pochi passi necessari ad atttivare la funzionalità multilingua per un’applicazione Laravel.

Attivazione e configurazione del middleware i18n

$ php artisan make:middleware Localization

Questo comando crea un nuovo file in app/Http/Middleware/Localization.php che va personalizzato se vogliamo gestire la lingua con le variabili di sessione. L’alternativa sarebbe specificare sempre in ogni rotta la lingua che si vuole usare. Un incubo!

Modifichiamo così il file:

    public function handle(Request $request, Closure $next)
    {
        if (Session::has('locale')) {
            App::setLocale(Session::get('locale'));
        }

        return $next($request);
    }

Con il comando setLocale si valorizza la lingua usata in quel momento dall’applicazione con il valore contenuto dalla variabile di sessione.

Configurazione della rotta per lo switch della lingua

Ora occorre un metodo per fare lo switch e lo facciamo in una rotta che scriviamo in routes/web.php:

Route::get('language/{locale}', function ($locale) {
    app()->setLocale($locale);
    session()->put('locale', $locale);
    return redirect()->back();
});

Possiamo anche scegliere di portare il codice che si trova nella funzione anonima dentro al controller HomeController.

Creazione dei file di localizzazione

Poi creiamo due file di lingua dentro a resources/lang/ e li chiamiamo en.json e it.json. Possiamo fare quanti file vogliamo, attenzione che ad ogni rotta deve corrispondere un file!

Per esempio un estratto del file italiano può essere il seguente:

{
    "Dashboard": "Cruscotto",
    "logout": "Esci",
    "Companies": "Aziende",
    "Users": "Utenti",
    "Name": "Ragione Sociale",
    "Website": "Sito web",
    "Email":  "Email",
    "Main Phone #": "Telefono principale",
    "latitude":  "Latitudine",
    "longitude": "Longitudine",
    "company_type":  "Tipo Azienda",
    "Commands": "Comandi",
    "Create new": "Nuovo record"
}

Comandi HTML per lo switch

Ora inseriamo i comandi per cambiare lingua nell’header dell’applicazione: lo facciamo in un modo grezzo che può essere migliorato, ma è questione di Blade e Vue:

                <div class="collapse navbar-collapse" id="navbarSupportedContent">
                    <!-- Left Side Of Navbar -->
                    <ul class="navbar-nav me-auto">
                        <li class="nav-item">
                            <a class="nav-link" href="dashboard/">{{ __('Dashboard') }}</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{{ route('companies.index') }}">{{ __('Companies') }}</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{{ route('users.index') }}">{{ __('Users') }}</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="/language/it">it</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="/language/en">en</a>
                        </li>
                    </ul>

Sostituire tutte le stringhe cablate con i segnaposto

Qui si può vedere anche come vengono utilizzati i segnaposto definiti in resources/lang: occorre sostituire ad esempio la stringa “Companies” cablata nell’HTML/Blade con il segnaposto {{__('Companies')}}.

Risorse web

Laravel primi passi: generare model, migrazione e seeding in un solo comando

Laravel logo
Laravel logo

La creazione di un model in Laravel può generare contestualmente anche altre classi, come le migrazioni per la tabella associata, il controller, le rotte resource, fino addirittura alle classi Faker/Seeder per riempire di dati di test la tabella.

$ php artisan make:model Role -mcrf

Questo comando oltre a creare il model creerà anche la migrazione (m), il controller (c), le resources (r) o azioni CRUD del controller. Inoltre creerà le classi Faker e Seeder (f) che consentono di specificare il tipo di dati di test con cui riempire le tabelle (Faker) e istruire il Seeder con il numero di record da creare.

Tutto in un solo comando!

Poi però occorre personalizzare ogni classe creata in base a cosa vogliamo fare.

Ho già creato il model, voglio creare le resources

Si possono dare i comandi anche in modo asincrono. In questo esempio:

$ php artisan make:controller UserController --resources

Sul package Faker

François Zaninotto è l’autore di questo package veramente formidabile. Egli ha deciso di abbandonare il progetto poiché “has a design problem”. Un difetto di progettazione iniziale ha fatto sì che le varie localizzazioni non potessero sessere scaricate separatamente dal pacchetto ma si dovesse scaricar tutto insieme.

Nel tempo le dimensioni del package con tutte le localizzazioni ha raggiunto dimensioni ragguardevoli e il numero di download è aumentato esponenzialmente (a fine 2020 era a 125 M download) e Zaninotto si è posto il problema dell’impatto ecologico in termini carbon footprint.

Be’ il suo calcolo lo ha impressionato, ha preferito chiudere e abbandonare il progetto, che ora è solo readonly.

Tutta la storia completa la potete leggere qui.

Altre risorse Laravel

Laravel: protezione con login Auth/Vue.js

Vue.js
Vue.js

Aggiungiamo Auth + Vue.js, il modo più diretto di aggiungere al progetto Laravel una protezione con password. Il plugin Auth è uno scaffold di autenticazione che utilizza Vue.js.

Installazione di Vue/Auth

composer require laravel/ui:^2.4
php artisan ui vue --auth

npm install
npm run dev
php artisan migrate

Il comando ui installa le componenti Vue.js e lo scaffolding necessario per le maschere di autenticazione, recupero password, eccetera e le funzionalità javascript delle interfacce.

npm

Il Node.js Packet Manager va invocato perché il prodotto Auth ha parecchie component javascript da installare.

Infine, il comando di migrazione serve per le tabelle dei recuperi password e dei token (“Ricordami questo accesso”).

Al termine dell’installazione saranno già state impostate tutte le rotte per Auth:

laravel route list con Auth
laravel route list con Auth

Vengono già predisposti i comandi di registrazione, recuper passord, login, logout. Però i conenuti rimangono ad accesso libero quidni va gestito cosa vogliamo proteggere con la login e cosa rimane publico.

Per fare questo occorre agire sul controller.

Per prtoteggere l’intera applicazione occorre fare in modo che ogni volta che si invoca la classe controller del modello che si vuole proteggere: avvenga l’interposizone del mìddleware Auth. Per esempio si può deìcidere tabella per tabella se si vuole la protezione oppure no:

class CompanyController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
    }

in questo caso il CRUD della tabella Company sarà intercettato dal middleware Auth e se si tenta di accedere ad una delle rotte, si presenterà la maschera di login:

Login
Login

Risorse

Laravel – impostare il nome dell’applicazione

Laravel logo

Il nome dell’applicazione può essere visualizzato nelle views di Laravel e può essere definito in almeno tre modi

file .env

Il file di configurazione delle variabil di ambiente è situato nella radice dell’albero dell’applicazione e si chiama .env.

Al suo interno si trova una variabile che possiamo personalizzare:

APP_NAME=MyBeautifulAppName

Poi, all’interno della view, ci riferiremo a questa variabile nel modo seguente

 {{env('APP_NAME')}}

Tuttavia il file .env non viene normalmente sottoposto a versione perché è caratteristico del singolo deploy su ogni server. Quindi tendo a non utilizzare questa variabile.

file config/app.php

Questo file contiene una variabile predefinita:

    'name' => env('APP_NAME', 'MyBeautifulAppName'),

Qui in realtà fa un’override del file .env, infatti utilizza la stessa variabile env, però è a runtime, cioè acquisisco il valore di default e lo metto in un’altra variabile name, eventualmente modificandolo. Per accedere al valore della variabile name in una view si scrive:

{{ config('app.name', 'Default name') }}

il secondo argomento essendo il default.

Condivisione di una variabile tra tutte le view

Il terzo metodo è il più elaborato ma consente una maggiore flessibilità: si definisce una variabile dal nome arbitrario nel file

<APP>/app/Providers/AppServiceProvider.php

e al suo interno, nel metodo boot(), si invoca il metodo statico View::share():

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        View::share('appTitle', "MyBeautifulAppName - powered by Laravel");
    }

La variabile è poi disponibile nelle view: {{$appTitle}}.

Se questi contenuti vi risultano utili, condivideteli. Ciao 🙂

Risorse web

Laravel – primi passi: localizzare un’applicazione con BabelEdit

BabelEdit è un programma che ci aiuta a localizzare l’applicazione. Si devono infatti personalizzare i file di localizzazione della lingua che già Laravel mette a disposizione nella cartella resources/lang. Fondamentalmente l’applicazione fa uso di etichette che poi recuperano per associazione il valore esteso del testo da visualizzare.

Per aiutare in questo tipo di attività, soprattuto se il lavoro di traduzione viene svolto da non programmatori, si può utilizzare questo software. Non sto facendo endorsement, voglio solo riportare una possibile soluzione cooperativa.

Download e installazione

Una volta installato, (al solito io uso Linux) lanciarlo con

$ BabelEdit
Dashboard di BabelEdit
Dashboard di BabelEdit

Utilizzo di BabelEdit

Ovviamente prima occorre dare l’OK per l’accettazione dei termini di licenza e per l’attivazione della Trial di 7 giorni.

Si carica il file delle etichette originarie (che sono quelle del plugin Auth):

Seleziona la sorgente dei dati
Seleziona la sorgente dei dati

Per il nostro progetto, i file di dizionario sono PHP. Si apre così la maschera per caricare i file da tradurre:

Configurazione dei linguaggi
Configurazione dei linguaggi

Si possono aggiungere i file origine e destinazione

BabelEdit aggiungere le lingue
BabelEdit aggiungere le lingue

Si associa il file alla lingua

Aggiunta nuova lingua
Aggiunta nuova lingua

Si ripetono questi passaggi per tutte le lingue che vogliamo trattare.

Cliccando su “Primary Language” selezioniamo qual è la lingua di riferimento da cui derivieremo tutte le traduzioni:

BabelEdit selezione lingua primaria
BabelEdit selezione lingua primaria

si apre la maschera di selezione del servizio

Babel configurazione del servizio di traduzione online
Babel configurazione del servizio di traduzione online

Apriamo un messaggio, vengono visualizzate le etichette così come sono state configurate nei due file di lingua. Cliccando in basso a destra lo scudo con stellina

Selezione del servizio di traduzione online
Selezione del servizio di traduzione online

si può vedere la traduzione proposta dal servizio online (ad esempio Google)

Babel traduzione automatica
Babel traduzione automatica

cliccando sulla proposta, il valore dell’etichetta viene modificato con quello proposto da Google. In assenza del servizio si può tradurre in autonomia.

Risorse web

Laravel – primi passi: il CRUD

CRUD con Laravel
Laravel logo

CRUD è l’acronimo per Create, Read, Update, Delete che sono le quattro operazioni fondamentali sui dati.

Laravel non ha molta automazione da questo punto di vista, dobbiamo costruirci le form HTML per creare e modificare i file, così come le viste per visualizzare tutti i dati oppure solo un record e alla fine la funzionalità di cancellazione.

Però ha un sistema di creazione di pagine di Blade che ci permettono il riutilizzo intelligente di molte parti di layout.

In questo aritcolo ci muoveremo su tutti e tre i domini del paradigma MVC.

CRUD 1: creiamo il template

Come prima operazione creiamo un template che riutilizzeremo in tutta l’applicazione. Lo creiamo in resources/views/layouts/app.blade,php:

<!doctype html>
<html lang="it">
<head>
    <meta charset="UTF-8">
    <title>Logistic Mapper</title>
</head>
<body>
<div class="container">

    @yield('content')

</div>

@yield('footer')

</body>
</html>

La direttiva @yield prenderà il contenuto della sezione content della vista chiamata e la metterà qui. Quindi se il controller chiama la view ‘edit.blade’ il suo contenuto verrà calcolato (cioè verranno messi dentro i dati) e poi prenderà posto dentro il tag “container”. È analoga ad una direttiva include. Quindi tutte le view avranno il titolo Logistic Mapper in questo caso.

CRUD 2: rassegna dei metodi da implementare

Create

Cominciamo con la creazione di nuovi records, operazione che nel dominio dei database è una insert.

La rotta per l’operazione create è la seguente:

| GET|HEAD  | company/create     | company.create  | App\Http\Controllers\CompanyController@create     |

Attenzione: questa azione consiste nella visualizzazione della form vuota (non stiamo ancora creando alcun record).
Il verbo HTTP a cui viene associata è GET e il nome della rotta è company.create. Ciò significa che la pagina web che dobbiamo progettare starà sotto /resources/view/company e si chiamerà create.blade.phpe verrà invocata dal metodo create del controller CompanyController.

La view che creiamo è questa:

@extends('layouts.app')

@section('content')
<h1>Brand New Company</h1>
<form action="/company" method="post">
    @csrf
<table>
    <tr>
        <th>ID</th>
        <th>Name</th>
        <th>Website</th>
        <th>Email</th>
        <th>latitude</th>
        <th>longitude</th>
        <th>company_type_id</th>
    </tr>
    <tr>
        <td>
            <input type="text" size="2" readonly name="id" >
        </td>
        <td>
            <input type="text" name="name" >
        </td>
        <td>
            <input type="text" name="website" >
        </td>
        <td>
            <input type="text" name="email" >
        </td>
        <td>
            <input type="text" name="latitude" >
        </td>
        <td>
            <input type="text" name="longitude" >
        </td>
        <td>
            <select name="company_type_id">
                @foreach($company_types as $v)
                <option value="{{$v->id}}">
                    {{$v->name}}
                </option>
                @endforeach
            </select>
        </td>
    </tr>
    <tr>
        <td colspan="7"><input type="submit"></td>
    </tr>
</table>
    <p><a href="{{route('company.index')}}"><< Companies</a></p>
</form>

@endsection

La prima direttiva (@extends) indica in quale template deve essere inclusa questa pagina web, ed è layouts/app.blade.php (blade.php è un suffisso standard, il formato in dot notation della vista è layouts.app).

La seconda dichiarazione @section è la sezione content, che è il frammento che dev’essere incluso nel template layouts.app.

Segue la form. Attenzione!

  • la action dev’essere semplicemente /company: la risorsa da invocare davvero non è decisa qui ma è decisa a livello della tabella di routing, in questo caso è il metodo CompanyController@create
  • il method dev’essere conguente con il metodo espresso nella tabella di routing , quindi GET.
  • c’è un’ulteriore direttiva @csrf che serve per evitare che il metodo venga chiamato via HTTP da un’altro sito. Ci siamo già occupati di questo attacco in questo post.

Come si vede ho una serie di campi di tipo testo e una combobox che devo popolare in anticipo. Il controller deve fare tutte queste cose:

    public function create()
    {
        $company_types= CompanyType::all();

        return view('company.create', compact('company_types'));
    }

La query su CompanyType serve appunto per procurarsi la combo box. Per questo si usa il metodo all() del model. Il risultato questo:

Create company
Create company

Store

DOpo aver compilato il modulo, occorre salvare il dato. L’operazion Eloquent equivalente all’insert come detto è store. Quindi per vedere come dobbiamo comprtarci dobbiamo studiarci la riga corrispondente della tabella di routing:

| POST      | company   | company.store | App\Http\Controllers\CompanyController@store     |

In questo caso il form HTML dovrà utlizzare il metodo POST che in questo caso viene preso davvero in consderazionie e non viene riscritto da alcuna direttiva. Inoltre il metodo di controller sarà CompanyController@store . Anche se è prevista la view company.store non si verrà usata perché verrà fatto un redirecto verso l’elenco di tutte le Company (company.index) in caso di successo.

Quindi il metodo store è il seguente:

    public function store(Request $request)
    {
        $company = new Company($request->all());
        $company->save();

        return redirect(route('company.index'));
    }

È semplicissimo: si crea un nuovo oggetto della classe Company inizializzandolo con i valori dell’array $request->all().

Attenzione! se tentiamo di passare come argomento semplcemente $request, Laravel s’incazza perché $request è un oggetto, mentre il costruttore del model vuole un array.

Il metodo save() di Eloquent si trasforma in una insert della tabella companies.

Finalmente, viene inviata al browser una direttiva di redirect verso la view company.index.

Edit

Il terzo metodo ci consente di editare i dati per poi eseguire un update. Quindi questo metodo serve per visualizzare la form di modifica e non per modificare i dati, operazioe che sarò in carico al metodo update.

Al solito ci facciamo guidare dalla tabella di routing

GET|HEAD |company/{company}/edit| company.edit |App\Http\Controllers\CompanyController@edit |

Qui l’URI di accesso alla pagiba di edit era già stato generato nella pagina index dalla funzione

route('company.edit', $v->id)

l’URI generato è proprio del tipo company/{company}/edit come indicato dalla tabella di routing, dove il segnaposto {company} è sostituito dal valore numerico della chiave primaria della company. Il verbo HTTP utilizzato anche in quetso è GET (Apache deve servirci una pagina web).

Il metodo edit del CompanyController deve caricare dal database i dati da inserire nella form di modifica:

    public function edit($id)
    {
        $company = Company::find($id);
        $company_type = CompanyType::all();
        $array = array(
            'company' => $company,
            'company_type' => $company_type,
            'company_type_id' => $company->company_type_id
        );

        return  view('company.edit', compact('array'));
    }

Viene caricato

  • l’array $company con i dati del record;
  • l’array $company_type
  • il valore $company_type_id che ci serve solo per posizionare correttamente la combo box; non sarebbe necessaria ma la scrittura della view ne risulta così un po’ più chiara.

La view è la seguente:

@extends('layouts.app')

@section('content')
<h1>Company edit</h1>
<form action="/company/{{$array['company']->id}}" method="post">
    @csrf
    @method('PUT')
<table>
    @if ($array['company']->id != null)
    <tr>
        <th>ID</th>
        <th>Name</th>
        <th>Website</th>
        <th>Email</th>
        <th>latitude</th>
        <th>longitude</th>
        <th>company_type_id</th>
    </tr>
    <tr>
        <td>
            <input type="text" size="2" readonly name="id" value="{{$array['company']->id}}">
        </td>
        <td>
            <input type="text" name="name" value="{{$array['company']->name}}" >
        </td>
        <td>
            <input type="text" name="website" value="{{$array['company']->website}}" >
        </td>
        <td>
            <input type="text" name="email" value="{{$array['company']->email}}" >
        </td>
        <td>
            <input type="text" name="latitude" value="{{$array['company']->latitude}}" >
        </td>
        <td>
            <input type="text" name="longitude" value="{{$array['company']->longitude}}" >
        </td>
        <td>
            <select name="company_type_id">
                @foreach($array['company_types'] as $v)
                <option value="{{$v->id}}" @if ($v->id == $array['company_type_id']) selected @endif>
                    {{$v->name}}
                </option>
                @endforeach
            </select>
        </td>
    </tr>
    <tr>
        <td colspan="7"><input type="submit"></td>
    </tr>
    @else
    <tr>
        <td colspan="7">No records found</td>
    </tr>
    @endif
</table>
    <p><a href="{{route('company.index')}}"><< Companies</a></p>
</form>

@endsection

Qui la riflessione più grossa la dobbiamo fare sulla action della form: essa deve invocare il metodo update, quindi, guardando la tabella di routing:

PUT|PATCH|company/{company}| company.update | App\Http\Controllers\CompanyController@update |

dobbiamo utillizzare il verbo HTTP PUT e impostare la action su company/{company}.

Il metodo di Controller update sarà

    public function update(Request $request, $id)
    {
        $company = Company::find($id);

        $company->update($request->all());

        return redirect(route('company.index'));
    }

Viene creato l’oggetto $company da una query sul model per ID, viene passato al metodo update l’array $request->all() e alla fine viene rediretto il browser sull’indice.

Perché per operazioni diverse siamo verbi HTTP diversi?

La RFC-2616 (Hyper Text Transfer Protocol HTTP/1.1) stabilisce che il metodo PUT va usato per sostituire una risorsa esistente nel server. Ci si ricordi che all’inizio HTTP era un protocollo che si usava solo per lavorare coi file.

Il metodo POST invece va usato per trasferire dati (nuovi) dal client al srever, quindi il risultato è la creazione di una nuova risorsa.

Per questo il metodo PUT è idempotente: se applicato più volte, produce lo stesso risultato.

Il metodo POST invece non lo è, ogni volta che lo si applica vengono create nuove risorse; questo è proprio il comportamento delle operazioni database di Update (PUT) e Insert (Post), per cui questo è il senso di utilizzare i verbi HTTP in modo appropriato per le varie operazioni.

Questa distinzione va fatta anche quando si progetta l’interfaccia API di un server.

Riferimenti

Laravel – primi passi: i Model

Abbiamo visto come intercettare una rotta (URI) e come gestirla con una funzione anonima, utilizzando sia una vista per visualizzare una pagina complessa che un output diretto.

Abbiamo poi visto come gestre questa rotta utilizzando un controller, e limitandoci a visualizzare una stringa di benvenuto.

Ora ci accingiamo a fare in modo che il controller esegua una query sul model in conseguenza ad una certa rotta. Per esempio abbiamo visto che esiste un metodo standard del controller chiamato index che effettuerà una query che estrae tutti i record del model (e quindi attraverso Eloquent, della tabella del database corrispondente a quel model).

Accesso diretto al database dal Controller (senza model)

Con questa modalità, che è quella dirty, sconsigliata, possiamo vedere come si possibile interagire direttamente con il database dal controller. Ma non è la prassi consigliata, lo facciamo vedere solo come primo approcio.

Occorre personalizzare il metodo index (ad esempio) del CompanyController come segue:

    public function index()
    {
        $companies = DB::select('select * from companies');
        return view('company.index', compact('companies'));
    }

la IDE si arrangia di solito ad importare le classi di cui abbiamo bisogno; in questo caso nel preambolo verrà aggiunto il namespace della classe DB per potervi accedere:

use Illuminate\Support\Facades\DB;

Accesso ai dati attraverso il model

Tuttavia impostando bene il model (dichiarando tutte le dipendenze uno a uno, uno a molti ecc) questi può fare molto meglio il lavoro per cui è sufficiente scrivere questo metodo, invece:

    public function index()
    {
        $companies = Company::all();
        return view('company.index', compact('companies'));
    }

il metodo all() è già definito nella classe Model di cui Company è un’estensione, non serve che lo scriviamo. Model ha una quantità di metodi già pronti per l’uso, più avanti ne vediamo anche un altro. Infatti la classe Company ha soltanto questa dichiarazione:

use Illuminate\Database\Eloquent\Model;

class Company extends Model
{
}

Dovremo personalizzare la classe Company solo se vorremo fare delle cose particolari.

Come si vede è estremamente semplice recuperare i dati attravero il model, non serve scrivere nessuna query, già Eloquent lo fa per noi.

Passaggio dei dati dal model alla view

L’altra “magia” che fa il controller è quella di inviare i dati ad una vista. Questa volta ci serve una pagina un po’ più elaborata del semplice output diretto.

Avendo utilizzato per la gestiione delle rotte il metodo resource() abbiamo tutte le rotte belle e pronte e serve creare per ogni rotta una pagina web sotto la directory resources/views/. La convenzione è che ogni view abbia una cartella dedicata ad ogni model: per esempio creeremo la cartella /resources/views/company e al suo interno definiremo tanti file quente sono le rotte da servire e cioè quanti sono i metodi del controller:

  • index
  • edit
  • create
  • ecc…

L’insieme delle rotte si può stampare con il comando php artisan route:list

route:list con resource
route:list con resource

Definiamo, tanto per partire due viste con Blade sotto /resources/views/company/ chiamandole con il nome del metodo del controller che le invocherà (anche questa è una convenzione da segurie per farci venire fuori i risultati con il minor sforzo):

  • index.blade.php (che conterrà la tabella con l’elenco delle company)
  • edit.blade.php (che conterrà la form di modifica di un record di company)

Il controller dovrà fornire alla view tutti i dati che vogliamo mostrare. Per esempio, per quanto riguarda la pagina delle company, vogliamo mostrae i dati della tabella company. Per questo mettiamo nella variabile $companies i dati ottenuti dal model come sopra e li passiamo alla view con la funzione compact($companies).

Nella view poi facciamo il ciclo. Una prima versione della view potrebbe essere questa, molto grezza:

@extends('layouts.app')

@section('content')
<h1>Companies</h1>
<table>
    <tr>
        <th>ID</th>
        <th>Name</th>
        <th>Website</th>
        <th>Email</th>
        <th>latitude</th>
        <th>longitude</th>
        <th>company_type_id</th>
    </tr>
    @if (count($companies) > 0)
        @foreach($companies as $v)
    <tr>
        <td><a href="{{ route('company.show', $v->id) }}\edit">{{$v->id}}</a></td>
        <td>{{$v->name}}</td>
        <td>{{$v->website}}</td>
        <td>{{$v->email}}</td>
        <td>{{$v->latitude}}</td>
        <td>{{$v->longitude}}</td>
        <td>{{$v->company_type_id}}</td>
    </tr>
        @endforeach
    @else
    <tr>
        <td colspan="7">No records found</td>
    </tr>
    @endif
</table>
@endsection

Del linguaggio Blade parliamo in un articolo a parte, qui ci interessa vedere come vengono acceduti i dati che il controller passa alla view. La view vede un array di oggetti che si chiama $companies e cicla su questo array prelevando per ogni ciclo gli attributi dell’oggetto-componente dell’array $v. Il risultato è il seguente, è molto grezzo ma ci consente di vedere che stiamo progredendo rapidamente:

Laravel: company index page
Laravel: company index page

Una attenzione particolare va alla costruzione del link che ci consente di navigare dall’elenco delle Company al singolo record per poterlo editare. Notate il costrutto veramente efficiente e semplice:

href="{{ route('company.edit', $v->id) }}"

la funzione route() chiama per nome la risorsa che gestisce l’edit; è la rotta che leggiamo dalla tabella sopra, generata con artisan:

GET|HEAD  | company/{company}/edit | company.edit    | App\Http\Controllers\CompanyController@edit

Laravel trasforma questa indicazione in un URI vero e proprio, assegnando anche l’id del record da modificare:

<a href="http://www.logisticmapper.local/company/1/edit">1</a>

View per l’editazione del record

In questa vista index.blade avevamo soltanto una variabile da visualizzare (che il controller passa alla view) che è il recordset. Più in generale alla view devono essere passati dati diversi che conviene impacchettare in un array associativo, come vediamo nel secondo esempio che invece riguarda la view che contiene la form HTML di gestione di un singolo record:

    public function edit($id)
    {
        //
        $company = Company::find($id);
        $company_types= CompanyType::all();
        $array = array(
            'company' =>$company,
            'company_types' => $company_types,
            'company_type_id' => $company->company_type_id
        );

        return  view('company.edit', compact('array'));
    }

Per questa view abbiamo bisogno di tre cose:

  1. il record da editare: queste informazioni vengono prelevate dal model Company, che è la rappresentazione ORM della tabella companies, con il metodo find() a cui passiamo l’$id del record che ci arriva dalla request ed è il parametro passato al controller. Il risultato è equivalente alla select sul record $id della tabella companies. Ma è molto meno stressante. Vale la pena di notare che tutto questo passaggio di parametri $id è fluido, non dobbiamo preoccuaprci del nome di campi e delle variabili, per il fatto che abbiamo adottato la convenzione.
  2. l’elenco delle company_types che deve consentirci di costruire la combobox omonima: per questo invochiamo un altro Model, CompanyType, che è quello che sovrintende alla tabella company_types. Qui ci servono tutti i valori della tabella per cui utilizziamo il metodo all() di Eloquent.
  3. il valore della chiave esterna company_type_id per questo record che ci deve aiutare a posizionare la combobox nella selezione corretta; lo estraiamo direttamente dall’oggetto $company e lo mettiamo a disposizione della vista in modo separato (non sarebbe necessario, ma ci consente di scrivere molto meno nella view).

Il controller quindi definisce una variabile $array che è un vettore associativo con le tre componenti che abbiamo enumerato sopra e che vengono passate alla view come array di oggetti con la funzione compact().

Finalmente la view che mostra la form di editing è la segente (company/edit.blade.php):

@extends('layouts.app')

@section('content')
<h1>Company</h1>
<form action="">

<table>
    @if ($array['company']->id != null)
    <tr>
        <th>ID</th>
        <th>Name</th>
        <th>Website</th>
        <th>Email</th>
        <th>latitude</th>
        <th>longitude</th>
        <th>company_type_id</th>
    </tr>
    <tr>
        <td>
            <input type="text" size="2" readonly name="id" value="{{$array['company']->id}}">
        </td>
        <td>
            <input type="text" name="name" value="{{$array['company']->name}}" >
        </td>
        <td>
            <input type="text" name="name" value="{{$array['company']->website}}" >
        </td>
        <td>
            <input type="text" name="name" value="{{$array['company']->email}}" >
        </td>
        <td>
            <input type="text" name="name" value="{{$array['company']->latitude}}" >
        </td>
        <td>
            <input type="text" name="name" value="{{$array['company']->longitude}}" >
        </td>
        <td>
            <select name="company_type_id">
                @foreach($array['company_types'] as $v)
                <option value="{{$v->id}}" @if ($v->id == $array['company_type_id']) selected @endif>
                    {{$v->name}}
                </option>
                @endforeach
            </select>
        </td>
    </tr>
    @else
    <tr>
        <td colspan="7">No records found</td>
    </tr>
    @endif
</table>
    <p><a href="{{route('company.index')}}"><< Companies</a></p>
</form>

@endsection

Il risutato è questo:

Laravel Model: company edit
Laravel Model: company edit

Riferimenti

Laravel – primi passi: i Controller

Proseguendo con l’invocazione delle rotte con Laravel, facciamo un riassunto: abbiamo visto come invocare l’applicazione usando una rotta. E abbiamo visto come si può usare il metodo statico Route::get() per associare un percorso URI (rotta) ad una funzione anonima. La quale al suo interno può emettere un output semplice (echo) oppure invocare una vista Blade.

Il passo successivo è quello di alzare il tiro ed effettuare una elaborazione più complessa a fronte dell’URI di richiesta.

Il componente naturale che viene chiamato per gestire una richesta è il controller. Come si ricorderà, il controller è il centro dell’applicazione (vedi l’articolo Laravel – primi passi – MVC) ed è quindi in questa sede che va gestito il processo.

Ma vediamo prima di tutto come creare un nuovo controller.

In generale dovremo creare un controller per ogni tabella del database:

una tabella -> un model -> un controller con diverse azioni -> diverse view, una per ogni azione, ma non solo.

Lo facciamo da Artisan con il quale, oltre a creare le migrazioni per defnire le tabelle, crea anche i Controller (e tantissime altre cose ancora):

$ php artisan make:controller CompanyController

Un piccolo inciso:

  • il comando è make:controller
  • il nome del controller va scritto in CamelCase con il nome della tabella al singolare. Questo perché se ci adeguiamo alla convenzione, ci risparmiamo tonnellate di file di configurazione da gestire (convention over configuration). In ogni caso è possibile derogare dalla convenzione agendo su opportuni file.

Per un sinottico delle convenzioni sui nomi delle variabili si consulti questo articolo.

Il comando sopra ci crea un controller vuoto (c’è sola la dichiarazione della classe), ma riempiamolo un po’. Se invece del precedente, lanciamo questo comando:

$ php artisan make:controller --resource CompanyController

il controller avrà al suo interno già definito 7 risorse che sono i metodi standard per effettuare le operazioni di CRUD sul modello:

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class CompanyController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index() // elenco di tutti i record
    {
        //
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create() // for di inserimento nuovo record
    {
        //
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request) // salvataggio in insert
    {
        //
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id) // mostra un singolo record
    {
        //
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function edit($id) // form di modifica di un record esistente
    {
        //
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id) // salvataggio in update
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id) // delete di un record
    {
        //
    }
}

Ora se personalizziamo nel modo seguente il metodo index():

    public function index()
    {
        return "This is the Company index";
    }

e definiamo la rotta così

Nota: dalla versione 8 di Laravel in poi, nella definzione della rotta, il controller deve essere specificato con tutto il suo namespace:

Route::get('/company', 'App\Http\Controllers\CompanyController@index');

il risultato sarà

Laravel: metodo index del controller
Laravel: metodo index del controller

È possibile anche passare parametri dalla rotta al controller. Per esempio lo faremo quendo vorremo mostrare un singolo record utlizzando il metodo show del controller; definiamo quindi la rotta nel file routes/web.php (nota il segnaposto {id} per il parametro della rotta):

Route::get('/company/{id}', 'App\Http\Controllers\CompanyController@show');

e quindi scriviamo il metodo show del CompanyController che gestice questa rotta(nota il parametro di ingresso $id):

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show(int $id):string
    {
        //
        return "This is company # ".$id;
    }

Il risultato è il seguente:

Metodo show per gestire la rotta
Metodo show per gestire la rotta

Ultimo paragrafo per resentare la rotta speciale resource che praticamente genera per noi tutte le rotte standard che riguardano uno stesso modello e tutti i controller stadard.

Avremo un rotta per l’index e un metodo di controller per l’index; una rotta di show e un cotnroller per lo show. e così va.

Basta creare una route di tipo resource:

Route::resource('company', 'App\Http\Controllers\CompanyController');

se ci facciamo visualizzare la tabella delle rotte da Artisan vediamo:

$ php artisan route:list

e questa è la tabella delle rotte che verrà visulizzata:

route:list con resource
route:list con resource

sono stati generati tutte le rotte, qassegnata ad ognuna un nome e associato il verbo del protocollo HTTP corrspondente (PUT/PATCH per update, POST per store e così via).

Se proviamo dal browser le varie rotte non avrò neanche un errore 404, nel senso che trova la risorsa che magari provoca un errore 500, oppure funziona: sono infatti da implementare le view che servono le rotte di tipo create, edit, update e delete. Inoltre, se si analizza la tabella delle rotte,si vedrà che l’URI non cambia al cambiare del metodo per show, update delete: in questo caso viene utilizzato un diverso verbo del protocollo HTTP!

Nel prossimo articolo farà la comparsa il model, vedremo così come il Controller chiede i dati al model.

Riferimenti

Laravel: convention over configuration

Laravel adotta lo schema convieni anziché configurare che in sostanza vuol dire adeguarsi ad uno standard convenuto per i nomi dei file in modo tale da evitare di dover scrivere e manutenere prolissi file di configurazione in cui, oggetto per oggetto, definiamo il nome che devia dalla convenzione.

Un esempio rende tutto chiaro.

Se abbiamo un modello CompanyType (in CamelCase al singolare), dovremo comportarci così per tutti gli altri oggetti

  • il nome della tabella è al plurale (company_types)
  • il nome del controller è al singolare (CompanyController)
  • i nomi delle chiavi esterne prendono dalla tabella al singolare e aggiungono _id (company_type_id)
  • i nomi delle rotte sono al plurale (/companies)

Più avanti estenderò questa lista aggiungendo altre convenzioni. Ma intanto riassumo n una tabella:

OggettoModalitàOKKO
Controllersingolare, CamelCasePostControllerPostsController
Routepluraleposts/1post/1
Named Routesnake_case con notazione puntoposts.show_activeshow-active-posts
Modelsingolare, CamelCasePostPosts
Tabella DBplurale, snake_casecompany_typescompany_type
Proprietà Modelsnake_case$model->created_at$model->createdAt
Foreign Keynome model singolare suffisso _idcompany_type_idcompanyTypeId, id_company_types
VariabilicamelCase$postAuthor$post_auhtor
Alcune convenzioni sul naming degli oggetti PHP per Laravel

Riferimenti