Questa è una vecchia versione del documento!


Laravel

Attenzione: Documento non completo in fase di stesura

Introduzione al framework Laravel

1. Installazione di Laravel

Introduzione a Laravel 5

Laravel è un framework PHP che negli ultimi anni ha avuto un discreto successo. Insieme a Symfony rappresenta una delle soluzioni più consolidate del momento per lo sviluppo di applicazioni Web in PHP.

Qualche mese fa è stata rilasciata la versione 5 di Laravel mentre quella attuale è la 5.1.11 e la minor release 5.1 è stata nominata come Long Time Support. E’ la prima volta che una release di Laravel riceve un supporto del genere quindi è opportuno considerare questa versione come il punto di riferimento dei framework PHP per i prossimi anni. La 5.1 segue lo standard PSR-2 che definisce una style guide nella scrittura del codice: i file del framework e tutti i file creati dai generatori di Laravel seguono questo standard e il consiglio degli sviluppatori è quello di mantenere tale convenzione anche per il codice delle proprie applicazioni.

Laravel presenta tutti i canoni dei framework Web moderni: ha una architettura interna basata su MVC (Model-View-Controller), si interfaccia con database relazionali tramite ORM (Object Relational Mapper) e ha un sistema di routing estremamente flessibile e configurabile.

Lumen

Dalla versione 5, gli sviluppatori di Laravel hanno introdotto un nuovo progetto collaterale: Lumen. Quest’ultimo rappresenta un microframework che utilizza alcune caratteristiche di Laravel ma in un contesto più snello e leggero che premia ovviamente le performance. Non tutti i componenti di Laravel sono presenti in Lumen, ma solo i principali. I requisiti software sono praticamente i medesimi. La versione attuale è la 5.1.0 e ricalca le funzionalità della versione 5.1 di Laravel.

API

Laravel incorpora molti componenti di Symfony e li estende in base alle proprie necessità e al proprio pattern di sviluppo. A questo indirizzo sono disponibili le API (Application Programming Interface) e la relativa documentazione. Il namespace di default di tutti i componenti è Illuminate. Tutti i componenti sono a disposizione anche su GitHub, scomposti in diversi progetti ciascuno per ogni macrocategoria.

Gli esempi presenti nella guida

La presente guida prenderà come riferimento la versione completa Laravel. L’applicazione che verrà realizzata sarà un gestionale per biblioteche e i sorgenti verranno rilasciati capitolo per capitolo in modo da offrire le diverse versioni implementate.

Installare Laravel con Composer

Laravel presenta un processo di installazione abbastanza snello basato su Composer e un elenco di requisiti preciso:

  • PHP versione 5.5.9 o successive;
  • L’estensione PHP OpenSSL per generare e verificare messaggi criptati;
  • L’estensione PHP PDO per la persistenza dei dati;
  • L’estensione PHP Mbstring per la gestione avanzata delle stringhe;
  • L’estensione PHP Tokenizer.

Nonostante sembrino estensioni particolari, esse sono relativamente molto comuni anche negli ambienti hosting più economici.

Composer

Composer si è imposto come standard industriale per gestire le dipendenze in PHP. Prima del suo avvento, il panorama PHP era molto disomogeneo, esistevano moltissime librerie ma trovare quelle più adatte al proprio progetto non era sempre facile, come non lo era l’integrazione in un’applicazione esistente.

Oltre a gestire le dipendenze, Composer permette anche il setup e la generazione di ambienti. Laravel si presenta come un framework altamente interoperabile con Composer, quindi una sua comprensione, seppur non approfondita, risulta quasi obbligatoria. Prima quindi di creare un nuovo progetto Laravel, è necessario avere Composer installato sul proprio ambiente.

Laravel e Composer

Una volta installato il tool, possiamo inizializzare la nostra applicazione utilizzando due diverse strategie: tramite l’installer di Laravel o tramite il generatore di Composer. Nel primo caso sarò necessario scaricare l’installer come pacchetto globale sulla nostra macchina

composer global require "laravel/installer=~1.1"

e lanciare l’eseguibile appena installato

laravel new ${applicationName}

Nel secondo caso basterà invece un semplicissimo

composer create-project laravel/laravel ${applicationName} --prefer-dist

In entrambi i casi i generatori creeranno la struttura base dell’applicazione nella cartella ${applicationName}.

Non esiste un metodo preferibile, ma secondo la documentazione ufficiale il primo risulta essere più veloce in quanto il download viene effettuato solamente nel momento dell’esecuzione dell’installer.

Composer, GIT e l’applicazione di esempio

L’applicazione appena installata, oltre alle folder di struttura definite da Laravel, presenta la cartella vendor che raccoglie le dipendenze di terze parti. Al momento dell’inizializzazione del progetto essa conterrà esclusivamente Laravel ma successivamente potrà contenere ulteriori dipendenze.

In caso di utilizzo di un sistema per il versioning (GIT o SVN), tale cartella non dovrebbe essere condivisa, questo per non appesantire la gestione del progetto con file che possono essere scaricati dalla Rete. Una volta ricevuto un progetto già esistente basterà eseguire un

composer update

per ottenere tutte le dipendenze dalla Rete.

L’applicazione di esempio seguirà questo approccio, quindi per avviarla, sarà necessario installare preventivamente le dipendenze.

Homestead

Oltre a rendere il processo di installazione e setup il più semplice possibile, gli sviluppatori di Laravel non si sono fermati qua. Un’alternativa rispetto a quella di creare un ambiente di sviluppo completo, può essere quella di usare Homestead.

Homestead è una macchina virtuale Vagrant già configurata con tutto il software necessario per lavorare con Laravel, ovvero:

  • l’OS Ubuntu;
  • PHP;
  • la virtual machine HHMV;
  • il server engine Nginx:
  • i DBMS MySQL e Postgres;
  • Node per eventuali tool per progetti clientside (bower, grunt, gulp…);
  • Redis e Memcached.

Questo significa che è possibile lavorare con Laravel, in un ambiente simile a quello finale di produzione, senza dover di fatto installare nulla sul proprio Pc. Vagrant si basa su VirtualBox o su VMWare per la virtualizzazione quindi richiede che almeno uno di questi strumenti sia installato. L’installazione non è banale e non è tema di questa guida quindi rimando alla guida di Homestead sul sito di Laravel e al sito ufficiale di Vagrant nel caso si verificassero particolari problemi.

L’idea di Homestead è sicuramente interessante. La prima installazione richiede un po’ di tempo in più, soprattutto per chi non conosce Vagrant, ma a lungo termine e se i progetti presentano aspetti non banali i vantaggi nell’utilizzare un sistema simile non tarderanno a palesarsi.

2. Laravel, il pattern di base: Facade

Dopo l’approfondimento incentrato sull’installazione del framework, concludiamo la parte di questa trattazione dedicata all’introduzione di Laravel con un contributo riguardante il più importante schema progettuale presente nel framework: il Facade Pattern. In pratica, questo design pattern viene utilizzato per tutte le funzionalità principali del framework e rappresenta la modalità più rilevante per creare componenti condivisi; per questa ragione, prima di procedere sarà necessario comprendere appieno il suo funzionamento.

Il Facade Pattern

Il Facade Pattern è un design pattern strutturale diffusamente utilizzato nella programmazione ad oggetti. Il termine “Facade” può essere tradotto come “facciata”, presto capiremo il perché. Questo pattern prevede la creazione un oggetto, il facade object, che fornisce un’API (Application Programming Interface) semplificata per una porzione di codice di dimensioni maggiori.

Tale meccanismo semplifica notevolmente le sessioni di sviluppo perchè permette di realizzare software caratterizzati da codici più facili da leggere e da utilizzare, riduce la catena delle dipendenze e facilita i test unitari in quanto esiste un unico componente aggregato. Il Facade Pattern è adottato frequentemente anche nei sistemi particolarmente complessi e quando i vari componenti software presentano parecchie interdipendenze tra di loro.

Esistono due altri pattern per molti versi simili al Facade che spesso vengono confusi con esso: l’Adapter Pattern e il Decorator Pattern. Nel primo caso, l’oggetto Adapter converte un’interfaccia per la programmazione in un’altra in base a cosa il consumer o l’utilizzatore si aspetta; nel secondo caso l’API originale viene arricchita o decorata per aggiungerne funzionalità avanzate o mancanti.

Facade Pattern in Laravel

Spostando l’attenzione su Laravel, gli oggetti facade permettono di accedere, da qualsiasi punto dell’applicazione, a quasi tutti gli oggetti disponibili nel container applicativo dei componenti, il ServiceContainer. Questo container permette al core di Laravel di gestire la dependency injection in maniera trasparente e funge da collante tra i vari componenti.

Per utilizzare una classe facade quando disponibile basterà invocarne staticamente i metodi dopo la necessaria inclusione. Non è obbligatorio specificare il namespace completo delle classi facade in quanto Laravel sfrutta degli alias per rimappare i percorsi completi in nomi più facili da ricordare. Di seguito viene proposto un esempio di utilizzo di una delle facade più importanti: Request:

<?php
namespace App\Http\Controller
 
use Request;
 
class TestController extends Controller
{
    public function testMethod() {
        echo Request::method(); 
        echo Request::url();
    }
}

In questo breve esempio sfruttiamo la classe Request, mappata tramite alias verso Illuminate\Support\Facades\Request, per recuperare metodo HTTP e URL della richiesta. E’ interessate però sapere che all’interno del file relativo (presente nella cartella vendor), non è presente nessun metodo statico method() o url().

L’unico metodo disponibile è getFacadeAccessor che determina il nome dell’oggetto presente nel ServiceContainer sul quale invocare il metodo relativo. Grazie al magic method di PHP __callStaticMethod è infatti possibile invocare il metodo sull’istanza dell’oggetto relativo che nel caso di Request è Illuminate\Http\Request.

Consultando questa pagina sarà possibile reperire l’elenco dei facade disponibili in Laravel e dei quali verrà fatto largo uso nei prossimi articoli.

Nel corso della guida e negli esempi che mano a mano allegheremo, faremo ricorso a tali oggetti con una certa regolarità e utilizzeremo questo pattern per creare una classe Facade da utilizzare più volte.

Le basi per la creazione di un progetto con Laravel

3. Laravel, struttura di un progetto e configurazioni generali

Dopo alcuni articoli introduttivi sul framework Laravel, è arrivato il momento di mettere in pratica quanto appreso e concentrarsi sugli aspetti più tecnici. Questo e i prossimi articoli saranno divisi in due parti; nella prima introdurremo le varie funzionalità di Laravel mentre nella seconda arricchiremo passo passo la nostra applicazione.

Struttura di un progetto

Una volta creata la nostra applicazione con la metodologia che preferiamo (tramite installer o tramite composer come da primo articolo della guida), quello che ci si presenta è una struttura di folder abbastanza complessa per chi si avvicina per la prima volta a questo strumento. La struttura può sembrare parecchio dispersiva e ridondante, ma una volta appresi gli scopi di ciascuna cartella tutto sembrerà più sensato. La struttura di un progetto Laravel è la seguente:

Analizziamo più nel dettaglio le diverse cartelle:

DirectoryDescrizione
appRappresenta il cuore pulsante dell’applicazione, tutti i file che implementano la business logic verranno creati in questa cartella; all’interno della cartella verranno posizionati anche i vari model
app/ConsoleContiene i comandi personalizzati che eseguiremo nella console
app/EventsContiene eventuali eventi personalizzati che potremmo creare per far comunicare diversi componenti tra di loro
app/ExceptionsContiene eventuali eccezioni custom utili
app/HttpContiene tutta la logica Web dell’applicazione suddivisa in sottocomponenti
app/Http/ControllersContiene tutti controller
app/Http/MiddlewareContiene tutti i middleware e i filtri
app/Http/RequestContiene tutti gli oggetti request definiti secondo lo standard PSR-7
app/JobsContiene tutti i queueable job dell’applicazione
app/ListenersContiene tutti le classi handler di eventi applicativi
app/ProvidersContiene tutti i Service Providers custom
bootstrapContiene i file principali per l’avvio di Laravel e una cartella cache per motivi di performance
configContiene tutte le configurazioni a livello applicativo
databaseContiene tutti i file relativi alla definizione del database
database/factoriesContiene le factory in grado di creare massivamente dei contenuti di test
database/migrationsContiene le definizioni storicizzate della struttura del database
database/seedsContiene script per il popolamento delle tabelle
publicRappresenta la document root del progetto e contiene tutte le risorse statiche pubbliche (JS, CSS, immagini, fonts…)
resourcesContiene file di logica ma non applicativi
resources/assetsContiene eventuali assets pubblici da compilare (Less, Sass, CoffeeScript, TypeScript…)
resources/langContiene i file per l’internazionalizzazione
resources/viewsContiene le viste PHP o blade
storageContiene file temporanei o cache
storage/appContiene file temporanei legati all’applicazione
storage/frameworkContiene file temporanei legati a Laravel
storage/logsContiene i log applicativi
testsContiene i file per i test automatici
vendorContiene, come da specifiche, tutte le dipendenze

Nella root del progetto sono presenti alcuni file descrittori da utilizzare con git, composer, gulp, npm, phpunit, l’eseguibile artisan e il file di configurazione dotenv.

Le configurazioni di un progetto Laravel

La prima cartella che richiede la nostra attenzione, è config. In questa directory sono presenti tutte le configurazioni dell’applicazione e di eventuali moduli aggiuntivi installati. I diversi contesti dell’applicazione sono configurati tramite file differenti in modo da offrire maggior manutenibilità: per esempio le configurazioni legate al database si troveranno in database.php mentre quelle per l’invio delle email in mail.php.

Spesso però alcune configurazioni possono differire tra i diversi ambienti; Laravel gestisce questa possibilità tramite la libreria DotEnv. Questa prevede la creazione di un file di configurazione .env nella root del progetto. Tutti i settaggi presenti verranno caricati all’interno dell’array globale $_ENV e grazie alla funzione env di Laravel è possibile impostarli all’interno del framework.

Una configurazione particolare è l’applicationKey. Questa permette di garantire un livello di sicurezza maggiore in quanto rappresenta la chiave di criptazione di sessioni e altri dati sensibili.

La regola generale quindi rimane quella di utilizzare DotEnv per quelle variabili che possono differire tra i diversi ambienti e di utilizzare direttamente i file nella cartella config per le configurazioni che non possono variare.

E’ buona norma non condividere il file .env con i vari membri del team o tramite git o svn. La miglior pratica è invece quella di mantenere un file .env.example con una configurazione base ma lasciare al singolo sviluppatore l’onere di definire il proprio file. Per questo motivo il file .env viene inserito di default dentro il file .gitignore autogenerato da Laravel in fase di setup iniziale.

Creazione e configurazione del progetto Biblios

Una volta appresi i concetti teorici, iniziamo a lavorare sulla nostra applicazione denominata “Biblios”. Tramite il comando:

composer create-project laravel/laravel biblios --prefer-dist

installiamo l’applicazione. Una volta scaricati i sorgenti grazie a composer dedichiamoci alla definizione dell’applicationKey tramite:

artisan key:generate

L’applicazione è ora pronta! Dal prossimo articolo ci addentreremo nei vari moduli del Framework. Ricordate di eseguire un composer update e di creare un file .env basandovi su .env.example.

4. Gestire il routing con Laravel

Cos'è il routing

I framework moderni per la realizzazione di applicazioni Web supportano il meccanismo del routing, un termine con il quale ci si riferisce alla gestione delle URL e il mapping tra esse e le funzionalità offerte dall'applicazione stessa. Nonostante possa sembrare una tematica di scarso interesse, avere delle URL ben formattate, leggibili e ricche di informazioni è un aspetto fondamentale per il SEO.Google e gli altri motori di ricerca premiano quei siti che offrono una navigazione parlante e, al contrario, svantaggiano i siti con URL dinamiche caratterizzate da parecchi parametri in querystring. La tendenza attuale è quella di spostare i parametri dalla querystring all'URL stesso. Per esempio un URL di questo tipo:

http://www.sito.it/post.php?idPost=1234

Potrebbe essere tradotto in:

http://www.sito.it/post/1234

o meglio ancora in

http://www.sito.it/post/titolo-del-post-normalizzato/1234

Nonostante questa opera di conversione possa sembrare complicata, utilizzando Laravel e il suo modulo di routing saranno sufficienti delle semplici operazioni.

Il file routes.php e le prime rotte di base

Per convenzione, le rotte di una applicazione Laravel sono definite all'interno del file app/Http/routes.php utilizzando il facade Route, automaticamente reso disponibile dal framework in questo file. Tale oggetto espone dei metodi che condividono il loro nome con i metodi HTTP standard (GET, POST, PUT, DELETE..) in modo da poter identificare non solo la URL ma anche il metodo che il client deve utilizzare per poter scatenare la funzionalità. Ecco alcuni esempi:

Route::get('/', function() {
    return 'Hello World';
});
Route::post('/post-url', function() {
    return 'Post is a beautiful method';
});

Nel caso fosse necessario mappare alcune URL su più di un metodo, possiamo usare i metodi match o any:

Route::match(['get', 'post'], '/', function() {
     return 'This is a GET or POST request';
});
Route::any('/any', function() {
    return 'I can respond to any http method';
});

Nonostante i form HTML non possano utilizzare metodi diversi da GET e POST, è comunque possibile invocare anche gli altri metodi utilizzando il parametro speciale _method.

Rotte parametriche e constraint

Una delle funzionalità più comode del modulo di routing di Laravel, è la possibilità di definire rotte parametriche esattamente come descritto nell'introduzione. Possiamo infatti scrivere:

Route::get('/post/{id}', function($id) {
    return "You requested post with ID = " . $id;
});

o, nel caso di parametri opzionali:

Route::get('/post/{id?}, function($id = 1) {
      return "You requested post with ID = " . $id;
});

Oltre ai parametri è impossibile impostare delle espressioni regolari di validazione sui parametri:

Route::get('/post/{id?}, function($id = 1) {
      return "You requested post with ID = " . $id;
})->where(['id' => '[0-9]+']);

Dare un nome alle rotte

Le applicazioni di una certa complessità richiedono un elevato numero di rotte. Per non perdersi tra tutte queste configurazioni è possibile dare un nome alle rotte in modo da poterle riutilizzare facilmente anche all'interno di altri componenti, come per esempio nelle viste.

Route::get('/post/{id?}, ['postDetail', function($id = 1) {
     [...]
});

Grazie alla funzione di utilità route è possibile infine recuperare una rotta senza la necessità di scriverla e mantenendo intatto il pattern DRY (Don't Repeat Yourself).

$route = route('postDetail', ['id' => 1]);

I gruppi di rotte

Spesso può capitare di dover impostare alcune configurazioni comuni a più rotte. Pensiamo eventualmente a prefissi di URL comuni o a meccanismi di localizzazione basati sulle URL. Laravel ci viene incontro grazie ai gruppi. Tramite questi ultimi è possibile “racchiudere” le rotte in uno o più insiemi configurabili; le configurazioni impostate a livello di gruppo saranno ovviamente rese disponibili a tutte le rotte presenti. In questo caso riusciamo a recuperare il terzo livello di un dominio per gestirne eventualmente la localizzazione.

Route::group(['domain' => '{locale}.site.com'], function () {
    Route::get('user/{id}', function ($locale, $id) {
       //
    });
});

Oppure possiamo anteporre un prefisso a tutte le URL:

Route::group(['prefix' => 'admin'], function () {
    Route::get('users', function ()    {
       // Matches The "/admin/users" URL
    });
});

Sono disponibili altre configurazioni a livello di gruppo (in particolare middleware e namespace), ma verranno introdotte successivamente, quando si parlerà degli altri componenti presenti in Laravel.

Biblios: le prime rotte e i primi test sull'applicazione

In questa seconda parte di implementazione della nostra applicazione denominata “Biblios” possiamo concentrarci sulle prime rotte. Al momento, dato che non abbiamo ancora introdotto i controller, definiremo temporaneamente la logica all'interno del file routes.php.

Apriamo quindi il file e aggiungiamo:

Route::get('/', [ 'as' => 'home', function () { //nomino la rotta 'home'
       return "Benvenuti in Biblios";
}]);
Route::get('/home', function () {
       return redirect(route('home'));
});

Per poter testare le rotte, utilizziamo il server HTTP integrato in Laravel. Apriamo quindi una console e, partendo dalla root del progetto, digitiamo:

php artisan serve 

(approfondiremo i comandi successivamente). Così facendo abbiamo avviato un server HTTP sulla porta 8000. Apriamo quindi il browser per navigare all'indirizzo

http://localhost:8000

In questo modo dovremmo visualizzare il nostro messaggio, mentre se puntiamo verso:

http://localhost:8000/home

dovremmo essere redirezionati sulla root del progetto.

5. I controller e la gestione delle richieste in Laravel

Associare una rotta ad un controller

Nonostante Laravel permetta di definire alcuni comportamenti direttamente nel fileroutes.php, questo approccio non è scalabile. La soluzione perfetta per gestire i vari entry point delle nostre applicazioni è invece l'utilizzo deicontroller. Laravel ha previsto per questa tipologia di componente una cartella dedicata:app/Http/Controllerse una classe da estendereIlluminate\Routing\Controller. Un controller è una semplice classe che presenta diversi metodi pubblici. Ciascuno di questi ultimi viene associato ad una URL e ad un metodo HTTP. Per associare una URL ad un determinato controller si usa la sintassi NomeController@nomeMetodo. Per esempio:

//routes.php
Route::get('/users', 'UserController@getList');
//UserController.php
class UserController extends BaseController {
    public function getList() {
        return 'User1, User2, User3';
    }
}

In questo caso abbiamo associato la URL /users al metodo getList dello UserController. Come per le rotte tradizionali, anche quando utilizziamo un controller è possibile dare ad esso un nome specifico:

Route::get('/users', [ 'uses' => 'UserController@getList', 'as' => 'userList' ]);

Eventuali parametri presenti nelle rotte verranno tradotti in parametri del metodo del controller invocato.

I controller RESTful

In caso di creazione di API restful, è possibile mappare automaticamente i metodi di una classe con le URL. Grazie al metodo resource, Laravel definirà al nostro posto tutte le rotte disponibili:

Route::resource('photo', 'PhotoController');

Le URL verranno create secondo questa tabella: Figura 1. Creazione delle URL. Creazione delle URL

Tramite il comando da console php artisan make:controller PhotoController è possibile creare uno scheletro di controller con i metodi già definiti.

Controller impliciti

Un'ulteriore funzionalità interessante è quella offerta dal metodo controller. In questo caso non sarà necessario mappare tutte le URL nel file routes.php, infatti, tramite una convenzione, Laravel creerà automaticamente le rotte in base ai metodi pubblici della classe. La convenzione prevede che i nomi del metodo siano composti dal nome del metodo HTTP seguiti dall'azione in camelCase e le rotte avranno come prefisso il nome del controller. Ad esempio il metodo getIndex del BookController verrà tradotto in una rotta GET all'URL /book (dato che “index” rappresenta la root) mentre il metodo postCreate verrà tradotto in una rotta POST all'URL /book/create.

Cachare le rotte

Se l'applicazione fa largo uso di rotte dinamiche definite tramite i metodi resource e controller, è buona norma quella di creare un file di cache per velocizzare il tempo di setup dell'applicazione (sulla documentazione ufficiale si parla addirittura di un risparmio di 100x). Basterà eseguire un php artisan route:cache per staticizzare la configurazione.

L'oggetto request

Una delle responsabilità dei controller è quella di agire sull'oggetto request. La miglior strategia per ottenere l'oggetto in questione è quella utilizzare la dependency injection di Laravel definendo un parametro tipizzato tra i parametri del metodo:

class DemoController extends BaseController
    public function dumpRequest(Illuminate\Http\Request $request) {
        var_dump($request);
    }
}

Una volta ottenuto l'oggetto, le funzionalità disponibili saranno molteplici.

Il metodo input dell'oggetto request permette appunto di leggere i parametri in ingresso. E' possibile passargli il nome del field da leggere ed eventualmente un parametro di default nel caso il parametro mancasse. Grazie invece al metodo all è possibile ottenere tutti i parametri.

$name = $request->input('name');
$email = $request->input('email', 'mail@domain.com');
$allParameters = $request->all();

Per ottenere un valore salvato in un cookie utilizziamo invece il metodo cookie:

$value = $request->cookie('cookieName');

L'oggetto response

La controparte dell'oggetto request è l'oggetto response. Quest'ultimo rappresenta appunto la risposta che il server invia al client. Normalmente i vari metodi mappati nel file delle rotte dovrebbero restituire un oggetto response, ma solitamente Laravel esegue il lavoro più complesso: quando restituiamo una semplice stringa, essa viene convertita automaticamente dal framework. Il seguente entrypoint:

Route::get('/', function () {
    return 'Hello World';
});

Potrà essere scritto anche come:

use Illuminate\Http\Response;
Route::get('home', function () {
    return new Response('Hello World');
});

o, utilizzando l'helper response:

Route::get('home', function () {
    return response('Hello World');
});

Il vantaggio di utilizzare l'oggetto response è però quello di poter agire anche su altre proprietà come ad esempio gli header o i cookie:

return response('Hello World)
  ->header('Content-Type', 'text/plain')
  ->withCookie('myCookieName', 'myCookieValue');

Oltre agli header è possibile personalizzare il tipo di response. Possiamo per esempio restituire una view (che analizzeremo nel dettaglio nel prossimo articolo):

return response()->view('viewName', array('name' => 'Alberto'));

o un listato in notazione JSON:

return response()->json(array('name' => 'Alberto', 'skills' => array('PHP')));

o ancora restituire un listato in notazione JSON in modalità JSONP:

return response()->json(...)->setCallback($request->input('cb'));

o un file in modalità download:

return response()->download('file.pdf');

L'ultima responsabilità della response è quella di rispondere con una redirect. In questo caso si utilizza l'helper redirect. Nel primo esempio scriviamo l'URL manualmente:

return redirect('home/dashboard');

mentre in quest'altro utilizzeremo una rotta particolare:

return redirect(route('myRoute'));

Il primo controller della nostra applicazione

In questa terza parte relativa all'implementazione dell'applicazione Biblios svilupperemo alcuni controller di base. La loro implementazione sarà ancora una bozza in quanto per la versione definitiva dovremo prima parlare delle view. Modifichiamo quindi il comportamento della rotta / facendola puntare ad un metodo del nostro FrontendController:

Route::get('/', [ 'as' => 'home', 'uses' => 'FrontendController@getHome']);

Dato che la nostra applicazione conterrà un elenco di libri, possiamo già definire la seconda rotta parametrica che si occuperà di mostrare il dettaglio di un libro.

Route::get('/book/{id}', [ 'as' => 'bookDetail', 'uses' => 'FrontendController@getBookDetail']);

Implementiamo il nostro controller inserendo, temporaneamente, il contenuto direttamente nel codice in attesa del refactoring. Dato che stiamo per scrivere la nostra prima classe è necessario prima di tutto definire il namespace. Laravel ci offre un comando per impostare il namespace all'interno sia della applicazione che di Composer. Lanciamo quindi:

php artisan app:name Biblios

Una volta definito il nostro scope, creiamo il controller:

namespace Biblios\Http\Controllers;
class FrontendController extends Controller {
	public function getHome() {
		return "Io sono la homepage";
	}
	public function getBookDetail($id) {
		return "Io sono il dettaglio del libro " . $id;
	}
}

I controller sono come al solito testabili tramite il comando php artisan serve. Una volta avviato sarà possibile navigare su http://localhost:8000 e http://localhost:8000/book/123.

6. Definizione delle views e template blade con Laravel

Le viste rappresentano il componente di Laravel che si occupa di definire la struttura delle pagine HTML e dei dati che verranno serviti agli utenti. Laravel supporta viste definite in semplici file PHP e viste Blade, un template system diffuso nel mondo della programmazione Web. Il riconoscimento dell'engine è automatico e basato su una convenzione a livello di filename (se la vista ha estensione .php verrà utilizzato l'engine classico, altrimenti se la vista ha estensione .blade.php verrà utilizzato l'engine di blade). Laravel favorisce l'utilizzo di Blade in quanto strumento più avanzato sia funzionalmente che sintatticamente rispetto ai classici file PHP.

View e Controller

Le views vengono definite principalmente all'interno dei controller una volta impostata la logica di business dell'applicazione. Tutto avviene tramite l'helper view:

return view('nomeview', $datiDaPassareAllaView);

Le views vengono automaticamente ricercate all'interno della folder resources/views. Sono disponibili anche subfolder all'interno del nome della view usando il carattere punto (.). L'estensione del file non viene mai comunicata esplicitamente delegando così a Laravel la scelta dell'engine. Per esempio, la view resources/views/user/detail.blade.php avrà come riferimento la stringa user.detail.

I parametri

I parametri alle views possono essere comunicati in due diversi modi, tramite il secondo parametro dell'helper view o tramite il metodo with. Nel primo caso l'oggetto dovrebbe essere un array associativo, mentre nel secondo caso può essere anche un oggetto plain. Nel dettaglio queste due righe sono a tutti gli effetti equivalenti:

return view('user.detail', [ 'username' => 'abottarini', 'email' => 'ab@gmail.com']);
return view('user.detail')->with('username', 'abottarini')->with('email', 'ab@gmail.com');

I view composer

I view composer sono componenti avanzati che permettono di condividere dati tra views diverse evitando di duplicare le logiche di business per l'ottenimento dei dati. Devono essere definiti all'interno di un service provider e permettono di associare ad una determinata view un particolare metodo (o una closure). In particolare con questo esempio comunichiamo al framework di invocare il metodo nomemetodo quando la view nomeview viene inclusa in altre views o invocata direttamente.

view()->composer('nomeview', 'nomemetodo');

Blade

Blade rappresenta il principale strumento per definire views all'interno di Lavarel. E' un linguaggio di templating compilato in file PHP e cacheato in modo da non generare nessun tipo di overhead. I file Blade hanno l'estesione .blade.php e grazie a questa Laravel li distingue da normali views PHP.

Ereditarietà

I template definiti con Blade sfruttano l'ereditarietà dei linguaggi orientati agli oggetti. E' infatti possibile definire layout padri astratti e layout figli che li estendono e aggiungono funzionalità e sezioni HTML. I costrutti principali per utilizzare l'ereditarietà sono @yield e @section:

<!-- views/layouts/master.blade.php -->
<html>
    <head>
        <title>@yield('title')</title>
    </head>
    <body>
        <div class="sidebar">
	  	@yield('sidebar')
	  </div>
 
        <div class="container">
            @yield('content')
        </div>
    </body>
</html>
<!-- views/view.blade.php -->
@extends('layouts.master')
 
@section('title', 'Page Title')
 
@section('sidebar')
    <p>This is my sidebar.</p>
@endsection
 
@section('content')
    <p>This is my content.</p>
@endsection

Il primo file rappresenta un layout, una sorta di scheletro astratto con tre sezioni configurabili, title, sidebar e container, definite tramite il costrutto @yield. Il secondo file rappresenta invece una view concreta che estende il precedente layout e ne definisce le sezioni tramite @section. Non c'è limite al livello gerarchico che si puo implementare. Possono esistere anche viste che estendono viste già complete con l'unico scopo di modificarne una sezione. Inoltre, grazie al costrutto @parent, è possibile embeddare in una vista figlia il contenuto del padre senza sovrascriverlo.

Stampa dei dati

Per visualizzare delle variabili all'interno dei template usiamo la sintassi {{ $nomevariabile }}. Uno strumento spesso sottovalutato è la possibilità di definire un default nel caso la variabile non sia definita: {{ $nomevariabile or 'stringa di default' }}. In automatico eventuali caratteri HTML vengono escapati da Laravel, ma nel caso fosse necessario stampare il valore della variabile senza filtri addizionali possiamo scrivere {!! $nomevariabile !!}.

Controllo del flusso

L'ereditarietà rappresenta la principale differenza di Blade rispetto agli altri linguaggi di templating. Oltre a questo aspetto, Blade presenta comunque tutti gli altri strumenti classici. Grazie a @if, @elseif e @else possiamo definire template condizionali. Utilizzando @for, @foreach, @forelse, @empty e @while possiamo renderizzare strutture HTML ripetitive all'interno di cicli. Tramite @include sarà possibile includere frammenti comuni all'interno di views (spesso sfruttando i ViewComposer descritti in precedenza).

Biblios parte: introduciamo le Views

La quarta parte del nostro tutorial ci permetterà di avere finalmente qualcosa di funzionante. L'obiettivo sarà quello di definire due views per i nostri controller getHome e getBookDetail e creare dei finti dati da mostrare agli utenti. Per iniziare possiamo modificare la nostra classe FrontendController aggiungendo due metodi privati che simulano l'interazione con il database e permettono di ottenere l'elenco dei libri disponibili e modificando i metodi pubblici come segue:

public function getHome() {
		return view('home')->with('books', $this->getAllBooks());
	}
 
	public function getBookDetail($id) {
		return view('bookdetail')->with('book', $this->getBook($id));
	}
 

In questo modo abbiamo creato la logica di business per visualizzare nella home l'elenco di tutti i libri e le pagine di dettaglio. Passiamo ora alle views. Da best practice il primo passo è quello di definire un layout astratto per poi passare alla definizione delle varie pagine. Creiamo quindi il file resources/views/layout/biblios.blade.php:

<html>
	<head>
		<title>Biblios - @yield('title')</title>
	</head>
	<body>
		<h1>@yield('title')</h1>
 
		<div id="content">
			@yield('content')
		</div>
	</body>
</html>

e estendiamolo grazie a resources/views/home.blade.php

@extends('layouts.biblios')
@section('title', 'Home')
@section('content')
	@foreach($books as $index => $book)
		<h2>{{ $book->title }}</h2>
		<p>
			{{ $book->author }}<br/>
			<a href="{{ route('bookDetail', [ 'id' => $index ]) }}">Guarda dettaglio</a>
		</p>
	@endforeach
@endsection

Oltre alla home definiamo il template della pagina di dettaglio. Una volta realizzato possiamo finalmente lanciare l'applicazione (con php artisan serve) e iniziare a navigare tra le diverse pagine del portale.

Eloquent e Laravel

Eloquent: l'ORM di Laravel

Dopo aver approfondito le componenti V (Views) e C (Controller) del pattern MVC, è giunto il momento di dedicarci alla M, il Model. Questo articolo rappresenta il primo di una serie dedicata alla gestione dei dati di business all'interno di una applicazione Laravel. Laravel mette a disposizione del programmatore un ORM particolarmente funzionale di nome Eloquent. Utilizzando Eloquent, ciascuna tabella del database viene mappata con una classe PHP che ne faciliterà la gestione permettendo di eseguire una serie di operazioni ignorando completamente lo strato di interazione con il database. Laravel supporta out-of-the-box 4 database differenti, mascherando le diverse implementazioni:

  • MySQL
  • Postgres
  • SQLite
  • SQL Server

Creazione e configurazione di modelli

Laravel offre un comando da terminale per la creazione di modelli. Grazie a php artisan make:model Model, il motore di generazione del codice creerà un file all'interno della cartella app. Il framework sfrutta appieno il pattern convention over configuration che prevede di avere una serie di convenzioni pre-definite dagli autori del framework e che spesso possono essere condivise. Tutte queste convenzioni possono comunque essere sovrascritte in casistiche particolari. La prima convenzione adottata riguarda la mappatura tra il nome del modello e il nome della tabella del database. La regola seguita è quella di convertire il nome del modello (spesso in camel case) in snake case e di pluralizzarlo. Quindi il modello User mapperà la tabella users e il modello BlogPost mapperà blog_posts. Questa convenzione è ovviamente sovrascrivibile tramite la property $table della stessa classe.

class User extends Model
{ 
    protected $table = 'name_of_the_users_table';
}

La seconda convenzione riguarda il campo chiave primaria. In automatico Laravel sfrutta il campo id, ma è possibile ridefinirlo utilizzando la property $primaryKey. Oltre al campo id, Laravel utilizza altri due campi standard per tutti i modelli: created at e updated at che contengono rispettivamente la data di creazione e di ultima modifica del modello. Laravel popola automaticamente questi campi ma è possibile disattivarli, se non necessari, impostando la property $timestamps su false. E' inoltre possibile personalizzare il formato di questi campi tramite la property $dateFormat.

Recuperare modelli dal database

Una volta configurati i modelli sulla nostra struttura delle tabelle del database, è giunto il momento di utilizzare i modelli nei controller. Le prime operazioni che analizzeremo saranno quelle di retrieve multiple e singole. Tramite il metodo statico all() è possibile recuperare tutte le entità di una particolare classe:

class UserController extends Controller
{
    public function index()
    {
        $users = User::all();
        return view('users', ['users' => $users]);
    }
}

Ciascun modello ottenuto tramite Eloquent è un oggetto PHP che presenta una serie di campi in base alle colonne della tabella sul database. Per esempio, per recuperare il valore username di un User, basterà scrivere $user→username. Grazie al motore di query building di Laravel, è possibile aggiungere particolari vincoli alle query di selezione dei record. Questo esempio permette di recuperare i primi 20 utenti, ordinati per data di creazione che presentano il campo enabled su “1”:

User::where('enabled', 1)->orderBy('created_at', 'asc')->take(20)->get()

Il motore di query building permette di concatenare una serie di direttive, la prima è rappresentata da un metodo statico (nel nostro esempio where) seguito da una serie di metodi; ognuno di questi ritorna un oggetto Query che può essere effettivamente eseguito tramite il metodo finale get(), che ritorna una Collection di record. Il precedente metodo all() rappresenta solamente un alias di un query builder. Nel caso volessimo ottenere un singolo record, e non una lista, abbiamo a disposizione due strumenti: il metodo statico find() che permette di recuperare un record a partire dall'id e il metodo first() che sostituisce il precedente get() e che ritorna solamente il primo record a partire dalle direttive configurate.

Inserire e aggiornare record

Il modo più semplice per creare ed aggiornare record è sfruttare le property PHP dell'oggetto e invocare il metodo save(). Sarà Laravel ad occuparsi della discriminazione tra le query di INSERT e di UPDATE.

$song = new Song;
$song->title = 'Everybody hurts';
$song->artist = 'R.E.M';
$song->save();
 
$song = Song::find(1);
$song->artist = 'R.E.M.';
$song->save();

Se dovesse servire, è possibile invocare una query UPDATE direttamente da criteri di selezione senza istanziare oggetti PHP:

Song::where('artist', 'R.E.M'.)->update('stars', 5);

E' importante ricordare che esiste un modo alternativo per creare un nuova entità tramite l'invocazione del metodo statico create() e un array associativo di proprietà. Questo approccio è comodo in caso di creazione con i parametri POST di una richiesta che vengono appunto forniti tramite un array associativo. Per evitare eventuali problemi di scrittura di proprietà private (come ad esempio la password) è possibile limitare i nomi dei campi impostabili tramite questa modalità di creazione di record. Con la property $fillable è possibile configurare i nomi dei campi accettati. Nel caso di approccio contrario, possiamo definire una blacklist di campi tramite $guarded. Non è invece possibile utilizzare entrambi i campi.

class Song extends Model
{
    protected $guarded = ['stars'];
}
[...]
Song::create([ 'title' => 'Losing my religion', 'artist' => 'R.E.M.' ]); //stars non è un attributo valido

Oltre al metodo create(), sono disponibili due altri metodi statici che possono ricevere un array associativo come parametro. firstOrCreate() permette di ottenere un istanza dal database o di crearla se non esiste; firstOrNew() è simile alla precedente ma in questo caso, se non presente, verrà solamente istanziata.

Eliminare record

Per eliminare un record già istanziato è possibile invocare il metodo delete(). Nel caso non si disponga dell'entità è possibile eliminare il record a partire dall'id o da una query:

$song = Song::find(1);
$song->delete();
 
Song::destroy(2);
 
Song::where('artist', 'Bieber')->delete();

Eliminazione logica

Laravel supporta out-of-the-box l'eliminazione logica dei record. Con “eliminazione logica” intendiamo la possibilità di gestire il classico “cestino” dove inserire i record eliminati: i record verranno sempre mantenuti all'interno del database ma verranno esclusi automaticamente da eventuali query di selezione. Per configurare l'eliminazione logica su un entità è necessario utilizzare il trait SoftDeletes nel modello e creare un campo deleted_at alla tabella relativa. Questo campo conterrà la data di eliminazione nel caso il record fosse stato cancellato e null nel caso opposto. Ad ogni invocazione di delete su entità del modello, il record non verrà eliminato, ma verrà valorizzata la colonna deleted_at. Sarà comunque possibile ricercare tra le entità eliminate logicamente tramite i metodi withTrashed() e onlyTrashed(). Nel caso si volesse invece annullare l'eliminazione è possibile utilizzare restore() e, nel caso opposto in cui si voglia fisicamente eliminare il record dal database, il metodo forceDelete().

Eloquent, le relazioni

Come tutti gli ORM che si rispettino, anche Eloquent è in grado di gestire le relazioni tra le entità in maniera facile e trasparente per l'utilizzatore. Laravel permette di implementare facilmente 6 tipologie di relazioni:

  • Uno a uno.
  • Uno a molti.
  • Molti a molti.
  • Molti a molti indiretta.
  • Polimorfiche.
  • Polimorfiche molti a molti.

Il modello presente in Eloquent per definire relazioni si basa sulla presenza di particolari metodi all'interno delle nostre classi. Questi metodi offrono una duplice funzionalità: invocando il metodo otterremo un oggetto programmabile (basato sui query builders di Laravel) per effettuare particolari operazioni, come ad esempio i filtri, mentre sfruttando le magic properties di PHP possiamo recuperare il valore della relazione come se fossero property.

$song->artist->name // restituisce 'R.E.M.'
$sont->artist() // restituisce l'oggetto che rappresenta la relazione

Come abbiamo visto nell'articolo precedente, Eloquent offre un sistema di convention over configuration eccellente che, se rispettato, permette di creare strutture di dati complesse praticamente senza scrivere alcuna riga di configurazione. Ovviamente il consiglio è quello di sfruttare al massimo le convenzioni e di sovrascriverle solamente se sono presenti impedimenti esterni non aggirabili.

Relazioni uno a uno

La relazione uno a uno è la tipologia di relazione più semplice e permette di avere un particolare modello collegato univocamente ad un altro modello. Un esempio di questa relazione potrebbe essere quella tra utente e indirizzo. Per definire tale relazione è sufficiente creare il metodo address nella classe User e invocare il metodo, ereditato da Model, hasOne.

class User extends Model {
    public function address() {
        return $this->hasOne('App\Address');
    }
}

Il metodo hasOne accetta tre parametri di cui solo il primo obbligatorio. Esso rappresenta il nome completo del modello relazionato. Il secondo parametro, opzionale, permette di definire il nome della colonna che rappresenta la foreign key verso l'entità principale. La convenzione definisce di avere una colonna denominata user_id all'interno della tabella addresses che conterrà l'id dell'utente correlato. Il terzo parametro serve nel caso si volesse utilizzare una colonna diversa dall'id per creare la relazione. Nel caso fosse necessario creare anche la relazione inversa, ovvero la possibilità di recuperare l'utente a partire dall'indirizzo, sarà necessario utilizzare il metodo privato belongsTo riportando sempre il nome del modello associato. Gli eventuali secondo e terzo parametro rispecchiano lo stesso valore semantico del metodo hasOne.

class Address extends Model {
    public function user() {
        return $this->belongsTo('App\User');
    }
}

E' importante ricordarsi queste due semplici regole:

  • il modello “forte” che non presenta, tra le sue colonne, riferimenti al modello “debole” necessita del metodo hasOne
  • il modello “debole” che presenta, tra le sue colonne, il riferimento al modello “forte” necessita del metodo belongsTo

Relazioni uno a molti

Una relazione uno a molti è similare alla precedente ma permette di associare un particolare modello con una lista di altri modelli che possono avere solamente un padre. L'esempio di prima potrebbe ritornare valido, se supponiamo che un utente può avere diversi indirizzi (per esempio di spedizione, di fatturazione, di residenza#8230;). Per definire questa relazione creiamo il metodo addresses invocando il metodo hasMany.

class User extends Model {
    public function addresses() {
        return $this->hasMany('App\Address');
    }
}

Come per il metodo hasOne, il secondo e il terzo parametro rappresentano rispettivamente il nome della colonna dell'entità debole e la chiave primaria dell'entità forte. Impostando la relazione in questo modo possiamo quindi ottenere la lista degli indirizzi associati ad un particolare utente:

User::find(123)->addresses

Sfruttando invece l'oggetto che rappresenta la relazione è possibile effettuare filtri:

User::find(123)->addresses()->where('city', 'Milano')->get()

Per definire la relazione inversa possiamo utilizzare il metodo belongsTo nello stesso modo analizzato per le relazioni uno a uno. Nel prossimo capitolo verranno analizzate le restanti tipologie di relazione, più articolate rispetto a quelle già esposte.

Eloquent, relazioni uno a molti, molti a molti e polimorfiche

Relazioni molti a molti

Le precedenti relazioni erano abbastanza semplici, rappresentavano in fondo la stessa tipologia di relazione e non introducevano strutture dati relazionali particolari, se non la necessità di avere una colonna in più nel database. Le relazioni molti a molti sono più complesse e necessitano la presenza di tre tabelle: due per rappresentare le entità e una, chiamata tabella pivot, per gestire le relazioni. Il nome di questa terza tabella è derivato dai nomi della altre due unendo i nomi singolari di esse, in ordine alfabetico, separati da un underscore. Un'implementazione di una classica wishlist presenterà quindi la tabella users, la tabella products e la tabella product_user. Quest'ultima dovrà contenere la colonna user_id e la colonna product_id. Per definire la relazione basta utilizzare il metodo belongsToMany. La relazione non ha modelli “deboli” e “forti”, quindi il metodo potrà essere utilizzato da entrambe le entità.

class User extends Model {
    public function products() {
        return $this->belongsToMany('App\Product');
    }
}
[...]
class Product extends Model {
    public function users() {
        return $this->belongsToMany('App\User');
    }
}

Eventuali parametri addizionali del metodo belongsToMany, utili solo nel caso non si rispettino le convenzioni, sono: nome della tabella pivot, nome della colonna che contiene l'id della prima tabella e nome della colonna della seconda tabella. Alcuni tipi di relazioni molti a molti necessitano la presenza di eventuali altre colonne all'interno della tabella pivot. Nel nostro piccolo esempio della wishlist potrebbe essere la data di inserimento del prodotto. Laravel permette di accedere ai dati addizionali della tabella pivot tramite una particolare property pivot a patto che questi dati siano evidenziati nella configurazione della relazione.

class User extends Model {
    public function products() {
        return $this->belongsToMany('App\Product')->withPivot('extraField');
    }
}
[...]
$products = User::find(1)->products();
$products[0]->pivot->extraField;

Relazioni uno a molti indiretta

Questo tipo di relazione rappresenta una scorciatoia nel caso di struttura complessa di entità relazionate tra loro secondo un modello uno a molti. Supponendo una struttura dati che modella le entità Artist, Album e Song, dove Artist e Album sono relazionati uno a molti così come Album e Song, tramite una relazione uno a molti indiretta è possibile risalire alle canzoni di un artista, passando, in maniera totalmente trasparente, dalla tabella degli album. Per definire questo tipo di relazione usiamo il metodo hasManyThrough.

class Artist extends Model {
    public function songs() {
        return $this->hasManyThrough('App\Song', 'App\Album');
    }
}

Le convenzioni utilizzate da Laravel sono quelle già viste in precedenza. Nel caso fosse necessario personalizzare qualcosa, è possibile utilizzare i parametri addizionali del metodo hasManyThrough.

Relazioni polimorfiche

Le relazioni polimorfiche sono una delle funzionalità di Eloquent più particolari e spesso non si trovano in altri ORM. Immaginiamo un modello dati che permetta di avere relazioni uno a molti ma con oggetti di natura diversa e un'unica entità correlata. Pensiamo per esempio alle entità Album, Artist e Photo e in particolare alla possibilità di avere foto sia legate all'album (per esempio la copertina) che all'artista. Ovviamente questo modello è implementabile con una doppia relazione uno a molti, ma questo sarebbe limitante nel caso si voglia considerare integra l'entità Photo. Le relazioni polimorfiche permettono appunto di avere un unica tabella Photo che è in grado di gestire relazioni con Album e Artist. Le tabelle Artists e Albums non presentano nulla di particolare, tutto è implementato nella tabella Photos. Oltre alle colonne di business, sarà necessario avere due colonne dedicate alla relazione, una contenente l'id dell'entità relazionata e uno contenente il tipo (che in questo modello potrà essere album o artist). Queste due colonne devono condividere un prefisso che rappresenta il nome astratto delle entità relazionate, per esempio coverable. Riassumendo quindi una struttura database per questo tipo di relazione potrebbe essere:

// tabella artists
id, name
 
// tabella albums
id, name, year, artist_id
 
// tabella photos
id, url, coverable_id, coverable_type

La definizione della relazione invece:

class Photo extends Model {
    public function coverable() {
        return $this->morphTo();
    }
}
class Artist extends Model {
    public function photos() {
        return $this->morphMany('App\Photo', 'coverable');
    }
}
class Album extends Model {
    public function photos() {
        return $this->morphMany('App\Photo', 'coverable');
    }
}

Grazie a questi metodi abbiamo creato una relazione bidirezionale. Sarà quindi possibile recuperare le foto di un album (o artista) in questo modo:

Album::find(1)->photos

e l'entità relazionata a partire dalla photo:

Photo::find(1)->coverable

Relazioni polimorfiche molti a molti

L'ultima tipologia di relazione configurabile in Laravel è una versione modificata delle relazioni polimorfiche che però include la possibilità di avere una relazione molti a molti tra l'entità morph e le altre. Riprendendo il nostro contesto musicale, possiamo creare una relazione di questo tipo partendo da Artists, Album, Songs e Genre e supponendo che un artista possa avere un genere principale, ma possa aver inciso album o canzoni di un diverso genere. In questo caso il genere è l'oggetto morph che si relaziona a ben 3 diverse entità . La struttura dati sarà simile a quella precedente se non per il fatto che viene creata una tabella pivot per gestire la relazione molti a molti:

// tabella artists
id, name
 
// tabella albums
id, name, year, artist_id
 
// tabella songs
id, name, album_id
 
// tabella genre
id, genre
 
// tabella genreable
id, genreable_id, genreable_type

Per definire la relazione all'interno delle nostre classi PHP utilizziamo i metodi morphToMany e morphByMany

class Artist {
    public function genres() {
        return $this->morphToMany('App\Genre', 'genreable');
    }
}
class Album {
    public function genres() {
        return $this->morphToMany('App\Genre', 'genreable');
    }
}
class Song {
    public function genres() {
        return $this->morphToMany('App\Genre', 'genreable');
    }
}
class Genre {
    public function artists() {
        return $this->morphedByMany('App\Artist', 'genreable');
    }
    public function albums() {
        return $this->morphedByMany('App\Album', 'genreable');
    }
    public function songs() {
        return $this->morphedByMany('App\Song', 'genreable');
    }
}

Eloquent e Migration in Laravel

L'ultima parte di questa trattazione dedicata ad Eloquent, il motore di ORM presente in Laravel, riguarderà le migration e i seeder, due strumenti per la gestione del database e dei dati tramite il framework. Nonostante la loro semplicità, si tratta di strumenti di vitale importanza nella definizione del database (migration) e di test (seeder).

Migration

Le migration rappresentano lo strumento per il versioning del database della nostra applicazione. Quando si lavora in team esistono diversi strumenti per il controllo delle versioni del codice, ma nessuno si occupa delle versioni del database: le migration servono proprio a questo. Ciascun file rappresenterà un'operazione da eseguire sul database applicativo. Questi file saranno ordinati e sarà possibile gestirli tramite appositi comandi come una vera e propria history, muovendosi in avanti e indietro tra le diverse versioni. Le migration verranno ospitate nella cartella database/migrations all'interno della nostra applicazione. Per creare un nuovo file esiste un apposito comando da terminale: php artisan make:migration nome_migration. Ciascuna migration è a tutti gli effetti una classe PHP che estende la super classe Illuminate\Database\Migrations\Migration. La classe presenta due soli metodi: up e down. Questi ultimi rappresentano rispettivamente il comando che si occuperà di creare nuove tabelle o colonne e il comando per annullare queste modifiche. Sarà compito del framework invocare il metodo up quando si vorrà aggiornare il database all'ultima versione e down in caso di revert delle modifiche. Una volta definita la nostra migration, la potremo attivare tramite comandi da terminale: ^Comando^Descrizione^ |php artisan migrate|Permetterà aggiornare il database all'ultima versione delle migration.| |php artisan migrate:rollback|Consentirà di tornare indietro di un gruppo di versioni.| |php artisan migrate:reset| Istruzione con cui sarà possibile tornare indietro alla prima versione.|

Creazione di nuove tabelle

Per creare una nuova tabella possiamo invocare il metodo create sulla facade Schema passando come parametro una callback che permette di definire le colonne della nuova tabella.

Schema::create('my_first_table', function (Blueprint $table) {
        $table->increments('my_first_id');
    $table->string('my_first_string_column');
});

Esistono diversi metodi per le varie tipologie di colonne. Un elenco esaustivo è disponibile nella documentazione ufficiale. Per rinominare una tabella utilizziamo:

Schema::rename('my_first_table', 'my_first_renamed_table');

Invece, per eliminarne una sarà possibile ricorrere a:

Schema::drop('my_first_table');

Creazione di nuove colonne

Per creare nuove colonne possiamo utilizzare un metodo simile a create: table.

Schema::table('my_first_table', function ($table) {
    $table->integer('my_first_integer_column');
});

Le colonne possono presentare anche dei modificatori, utili a meglio descrivere la struttura dei dati. Per esempio per definire una colonna come nullable scriviamo:

Schema::table('my_first_table', function ($table) {
    $table->integer('my_first_nullable_column')->nullable();
});

Eventuali altri modificatori sono first, after (per definire la posizione della colonna nella tabella), default (per impostare un valore di default) e unsigned (per impostare la colonna numerica come unsigned). Per modificare o rinominare colonne già esistenti possiamo utilizzare:

Schema::table('my_first_table', function ($table) {
    $table->string('my_first_string_column', 50)->nullable()->change(); //imposto la lunghezza a 50 e la rendo nullable
});
 
Schema::table('my_first_table', function ($table) {
    $table->renameColumn('my_first_string_column', 'my_first_renamed_string_column');
});

Questi ultimi comandi, dato che richiedono query SQL non banali per essere eseguite, necessitano della presenza di un modulo addizionale di Laravel installabile tramite composer: doctrine/dbal. Per eliminare una colonna si ricorrerà invece a:

Schema::table('my_first_table', function ($table) {
    $table->dropColumn('my_first_dropped_column');
});

Indici e chiavi esterne

Grazie alle migration è anche possibile definire aspetti più avanzati come indici e chiavi esterne. Per quanto riguarda gli indici abbiamo a disposizione diversi modificatori: unique (per creare un indice unique), primary (per impostare una chiave primaria) e index (per creare un indice standard). Le chiavi esterne invece possono essere create con:

$table->foreign('external_table_id')
        ->references('id')->on('external_table')
        ->onDelete('cascade');

Eloquent e Seeder in Laravel

I seeder

I seeder rappresentano l'altra faccia della medaglia delle migration e si occupano dell'inserimento di dati temporanei nel database. Questi permettono di avere dei dati di test caricati sul database a scopo dimostrativo. A differenza di quanto accade con le migration, data la loro natura i dati inseriti dai seeder non devono rappresentare informazioni utili per il setup dell'applicazione. Come per le migration è possibile creare un nuovo seeder tramite un'istruzione inviata da linea di comando sulla base della seguente sintassi:

php artisan make:seeder NomeDelSeeder

I seeder sono classi PHP ospitate nella cartella database/seeds che presentano un unico metodo run che dovrà contenere la logica per l'inserimento dei dati di test. Questi ultimi possono essere inseriti sia attraverso del codice ad hoc che tramite factory. Per il primo caso è sufficiente sfruttare le API query builder o direttamente Eloquent per l'inserimento di nuovi record:

DB::table('my_first_table')->insert([
        'name' => str_random(10),
        'age' => rand(10,40)
    ]);

Nel caso fosse necessario inserire una mole di dati più corposa possiamo utilizzare le Model Factory.

Model Factory

Le Model Factory permettono di definire, per ciascun modello dell'applicazione, logiche per la definizione di oggetti partendo da dei modelli fake, cioè a solo scopo di test. Tramite la libreria Faker, inclusa direttamente in Laravel, sarà possibile creare oggetti sempre diversi in pochissimo tempo. Le factory vengono definite all'interno della cartella denominata database/factories e sono composte di fatto da una singola callback, utile a definire la struttura dei modelli.

$factory->define(App\Album::class, function (Faker\Generator $faker) {
    return [
    'name' => $faker->name,
    'year' => $faker->year,
    'track_count' => rand(5,15)
    ];
});

Una volta definita la factory, sarà possibile invocarla all'interno del seeder tramite l'helper factory:

factory(App\Album::class, 50)->create(); //50 album creati con un unico comando.

Invocare i seeder

Per eseguire i seeder la prima cosa da fare è quella di aggiungere il riferimento all'interno della classe che funge da entry point: DatabaseSeeder. Grazie al metodo call possiamo aggiungere tanti seeder quanti ne desideriamo. Una volta fatto ciò, possiamo lanciare il comando

php artisan seed

per invocare tutti i seeder configurati. Tramite l'istruzione

php artisan db:seed --class=MySeeder

possiamo poi limitare l'esecuzione ad un solo seeder. Con questo capitolo si chiude la parte teorica dedicata a Eloquent, nel prossimo sfrutteremo le competenze acquisite per incrementare le funzionalità dell'applicazione “Biblios”.

Sviluppare un progetto con Laravel

Database, modelli, Factory e seed

Setup del database

Dopo la serie di articoli dedicati ad Eloquent è ora di mettere in pratica quanto appreso nel nostro progetto. La prima cosa da fare è quella di configurare i parametri di connessione al database. Il file di configurazione dedicato al database è banalmente config/database.php ma il miglior modo di configurare i parametri è utilizzare il file .env in modo da poter riutilizzare gli stessi file su diversi ambienti; .env contiene infatti tutte le configurazioni dipendenti dall'ambiente. Le property interessate sono:

PropertyDescrizione
DB_CONNECTIONContiene la tipologia di database utilizzato, di default MySQL
DB_HOSTContiene l'ip o l'indirizzo del server db
DB_DATABASEContiene il nome del database
DB_USERNAMEContiene lo username
DB_PASSWORDContiene la password

I primi modelli che creeremo all'interno della nostra applicazione saranno Book e Author. La relazione che intercorrerà tra di essi sarà la classica uno-a-molti basandoci sulla convenzione che un libro abbia un solo autore e lo stesso autore può aver scritto più libri. Oltre a questa relazione esisterà anche una relazione molti-a-molti tra utente e libro che rappresenta la lettura di un libro: un utente può aver letto più libri e un libro può essere stato letto da più utenti. L'entità User viene definita di default da Laravel e non ci sarà bisogno di crearla, sia a livello di classe PHP sia di migration. Partiamo quindi con la creazione delle tabelle tramite le relative migrations. I comandi da eseguire sono:

php artisan make:migration create_books_table
php artisan make:migration create_authors_table
php artisan make:migration create_book_user_table

La prima tabella (books) rappresenta l'entità Book, la seconda tabella (authors) rappresenta l'entità Author e la terza tabella (bookuser) rappresenta la relazione molti-a-molti tra User e Book. Tutte le migrations dovranno implementare sia il metodo up che il metodo down. I metodi up si occuperanno della definizione vera a propria della tabella, mentre i down solo della loro eliminazione:

// metodo up di create_books_table
Schema::create('authors', function (Blueprint $table) 
    {
        $table->increments('id');
        $table->string('firstname');
        $table->string('lastname');
        $table->timestamps();
    });
 
// metodo up di create_authors_table
Schema::create('books', function (Blueprint $table) 
    {
        $table->increments('id');
        $table->string('title');
        $table->integer('author_id')->unsigned();
        $table->timestamps();
    });
 
    Schema::table('books', function (Blueprint $table) 
    {
        $table->foreign('author_id')
            ->references('id')->on('authors')
            ->onDelete('cascade');
    });
 
// metodo up di create_book_user_table
Schema::create('book_user', function (Blueprint $table) 
    {
        $table->integer('book_id')->unsigned();
        $table->integer('user_id')->unsigned();
        $table->timestamps();
    });
    Schema::table('book_user', function (Blueprint $table) 
    {
        $table->foreign('book_id')
            ->references('id')->on('books')
            ->onDelete('cascade');
        $table->foreign('user_id')
            ->references('id')->on('users')
            ->onDelete('cascade');
    });

Nel primo file vengono create 3 colonne (id, firstname e lastname) e le colonne createdat e updatedat grazie al metodo dedicato timestamps(). Nel secondo file vengono create 3 colonne (id, title e author_id) e le colonne timestamps. Viene inoltre creato una chiave esterna verso la tabella authors. Da notare che la chiave esterna viene creata tramite una seconda query, in modo da avere la struttura dati già definita. Nel terzo file, quello della tabella pivot, vengono create le due colonne contenenti le chiavi esterne e i metodi timestamps. Una volta definite le migrations, lanciamo php artisan migrate e, se tutto ok, dovremmo ritrovarci con le 3 tabelle create sul database (più l'eventuale tabella users creata di default da Laravel). Passiamo ora alla definizione delle classi PHP che rappresentano i modelli. Da linea di comando lanciamo:

php artisan make:model Book
php artisan make:model Author

e andiamo a configure le relazioni utilizzando i metodi di Eloquent:

// User.php
public function books()
{
    return $this->hasMany('Biblios\Book')->withTimestamps();
}
 
// Book.php
public function author()
{
    return $this->belongsTo('Biblios\Author');
}
public function users()
{
    return $this->hasMany('Biblios\User')->withTimestamps();
}
 
// Author.php
public function books()
{
    return $this->hasMany('Biblios\Book');
}

La configurazione è abbastanza triviale, grazie al fatto che abbiamo utilizzato i nomi convenzionali per le tabelle e le colonne. L'unica funzionalità degna di nota è l'utilizzo di withTimestamps() in coda a hasMany in modo da avere accesso alle informazioni temporali all'interno della tabella pivot di relazione. Il primo step è quindi completato. Prima di andare oltre possiamo testare quanto fatto utilizzando tinker, la console disponibile in Laravel. Lanciamo quindi php artisan tinker e eseguiamo il comando:

use Biblios\Author;
use Biblios\Book;
$author = new Author();
$author->firstname = 'Nome autore';
$author->lastname = 'Cognome autore';
$author->save();
$book = new Book();
$book->title = 'Prova titolo';
$book->author()->associate($author);
$book->save();

Se tutto è stato implementato correttamente non dovremmo ottenere errori.

Factory e seed

Lo step successivo prevede di definire dei seed che si occuperanno di creare dei dati di test nel database. Le factory sono classi che si occupano di costruire istanze di particolari modelli inserendo dei dati di test nelle loro property. Creiamo quindi 2 file all'interno di database/factories ciascuno dedicato ad un modello: AuthorFactory e BookFactory.

// author factory
$factory->define(Biblios\Author::class, function (Faker\Generator $faker) 
{
    return [
        'firstname' => $faker->firstName,
        'lastname' => $faker->lastName
    ];
});
 
// book factory
$factory->define(Biblios\Book::class, function (Faker\Generator $faker) 
{
    return [
        'title' => $faker->sentence(rand(1,5))
    ];
});

Dopo le factory possiamo definire il nostro seeder con il comando: php artisan make:seed AuthorBookSeeder. All'interno del file run del nostro nuovo file inseriamo quindi questa porzione di codice per creare 10 autori e 10 libri:

factory(Biblios\Author::class, 10)->create()->each(function($author) 
    {
        foreach(range(1, 10) as $i) 
        {
            $author->books()->save(factory(Biblios\Book::class)->make());
        }
    });

Il seed può essere avviato con php artisan db:seed e se tutto è stato implementato correttamente dovremmo ritrovarci un database popolato.

Controller e interfacce per le operazioni CRUD

I primi controller CRUD

Dopo aver definito la struttura dei dati e dopo aver introdotto una reale persistenza dei dati, possiamo finalmente concentrarci sui controller più o meno ufficiali per il nostro progetto. La prima funzionalità che aggiungeremo metterà a disposizione del cliente alcune interfacce per le operazioni CRUD (Create, Read, Update, Delete) sulle varie entità disponibili. L'acronimo CRUD incapsula tutte le funzionalità base che vengono spesso implementate nei backoffice. Per il momento ci concentreremo sulle funzionalità core e tralasceremo altri aspetti come le performance e la sicurezza, questi argomenti, non secondari, li affronteremo in futuro. Il core di Laravel non presenta un modulo per la gestione dell'HTML e dei form, ma esiste una dipendenza esterna, chiamata Laravel Collective Form, che permette di creare form in maniera dichiarativa e object-oriented. Laravel Collective è una suite che può includere diverse funzionalità, per il momento utilizzeremo solamente il modulo HTML. Una volta installato tramite Composer, dobbiamo configurare il provider e i due alias. Per il momento ignoriamo lo scopo di queste attività in quanto le approfondiremo nelle prossime lezioni. Passiamo ora alla creazione dei controller tramite i comandi Laravel. Lanciamo quindi php artisan make:controller AuthorController e php artisan make:controller BookController e configuriamo le rotte in questo modo per il mapping automatizzato tra rotte e metodi:

Route::resource('author', 'AuthorController');
Route::resource('book', 'BookController');

L'implementazione dei controller è molto simile, all'interno dell'articolo approfondiremo in dettaglio solamente AuthorController e per quanto riguarda BookController le sole caratteristiche peculiari.

AuthorController

Partiamo quindi con la definizione del controller AuthorController analizzando metodo per metodo, ricordando che ogni metodo corrisponde ad un URL mappato automaticamente tramite Route::resource. Il metodo index si occupa di mostrare l'elenco degli autori disponibili:

public function index()
{
    $authorList = Author::orderBy('firstname', 'asc')->orderBy('lastname', 'asc')->get();
    return View::make('author/index')->with('authorList', $authorList);
}

Il metodo è abbastanza banale: tramite Eloquent recuperiamo l'elenco degli autori ordinati per firstname e lastname e passiamo la lista ottenuta alla view author/index. I metodi create e store si occupano della creazione di un autore. Il primo permette di inizializzare un form vuoto, mentre il secondo della validazione e della persistenza:

public function create()
{
    $author = new Author();
    return View::make('author/form')->with('author', $author);
}
 
public function store(Request $request)
{
    $this->validate($request, Author::$rules);
    Author::create($request->all());
    return Redirect::to(route('author.index'));
}

Il primo metodo non fa altro che istanziare un nuovo autore vuoto e di passarlo a author/form, store invece invoca la validazione e in caso positivo salva l'autore all'interno del database e redirige l'utente alla pagina precedente. Riguardo alla validazione, l'implementazione di store utilizza il metodo validate, ereditato dal controller grazie al trait ValidatesRequests che richiede l'oggetto request e un elenco di regole di validazione, nella fattispecie la proprietà statica $rules presente in Author. Avere le regole di validazione definite dentro il modello è una prassi consolidata in modo da poterle condividere senza doverle ripetere. Ecco come sono implementate:

public static $rules = [
    'firstname' => 'required|min:3|max:40',
    'lastname' => 'required|min:3|max:40'
];

I metodi edit ed update rappresentano gli speculari di create e store ma si occupano della modifica di un record esistente. Il primo recupera l'autore corrente dal database mentre il secondo valida e persiste:

public function edit($id)
{
    $author = Author::findOrFail($id);
    return View::make('author/form')->with('author', $author);
}
 
public function update(Request $request, $id)
{
    $this->validate($request, Author::$rules);
    $author = Author::findOrFail($id);
    $author->update($request->all());
    return Redirect::to(route('author.index'));
}

Rispetto ai due metodi precedenti l'unica differenza risiede nel fatto che non partiamo da un autore vuoto, ma lo recuperiamo, grazie a findOrFail dal database. L'ultimo metodo si occupa dell'eliminazione di un record:

public function destroy($id)
{
    Author::destroy($id);
    return Redirect::to(route('author.index'));
}

Una volta definiti i metodi passiamo alle view: index e form (riutilizzato sia in fase di create che di edit).

@section('content')
    <p>
        <a href="{{ route('author.create') }}">Create new author</a>
    </p>
    <table class="table">
        @foreach($authorList as $author)
            <tr>
                <td>{{ $author->firstname }} {{ $author->lastname }}</td>
                <td>{{ $author->bookCount }}</td>
                <td>
                    <a class="btn btn-default" href="{{ route('author.edit', ['author' => $author->id]) }}">Modifica</a>
                </td>
                <td>
                    {!! Form::open(['route' => ['author.destroy', $author->id], 'method' => 'delete' ]) !!}
                        {!! Form::submit('Elimina', ['class' => 'btn btn-danger']) !!}
                    {!! Form::close() !!}
                </td>
            </tr>
        @endforeach
    </table>
@endsection

index.blade.php si occupa di mostrare all'interno di una table i record ottenuti dal controller e di inserire i link per la modifica e l'eliminazione. Da notare che il metodo delete deve essere invocato utilizzando POST, quindi è necessario mascherare il form con un pulsante.

@section('content')
 
    {!! Form::model($author, [
        'route' => isset($author->id) ? ['author.update', $author->id] : 'author.store', 
        'method' => isset($author->id) ? 'put' : 'post'
        ]) !!}
 
        <div class="form-group {{ $errors->has('firstname') ? 'has-error' : '' }}">
            {!! Form::label('firstname', 'Firstname') !!}
            {!! Form::text('firstname', null, ['class' => 'form-control']) !!}
            @foreach($errors->get('firstname') as $error)
                <span class="help-block">{{ $error }}</span>
            @endforeach
        </div>
 
        <div class="form-group {{ $errors->has('lastname') ? 'has-error' : '' }}">
            {!! Form::label('lastname', 'Lastname') !!}
            {!! Form::text('lastname', null, ['class' => 'form-control']) !!}
            @foreach($errors->get('lastname') as $error)
                <span class="help-block">{{ $error }}</span>
            @endforeach
        </div>
 
        <div class="form-group">
            {!! Form::submit('Save', ['class' => 'btn btn-primary']) !!}
        </div>
 
 
    {!! Form::close() !!}
 
@endsection

La seconda view, form.blade.php, è senza dubbio più interessante. Innanzitutto si nota come il form viene instanziato grazie all'helper Form::model che permette di associare automaticamente i campi HTML con le proprietà del modello passato come parametro. Inoltre differenziamo la rotta in base alla presenza o meno dell'id (se non ha id sarà una create altrimenti una edit). Per ogni campo viene mostrata label, campo di testo (gestito automaticamente da Form::model) e l'eventuale messaggio di errore.

BookController

Il secondo controller è sviluppato con gli stessi pattern visti in precedenza tranne per alcuni aspetti. Per esempio nel metodo index utilizziamo with per usare l'eager loading e ottimizzare il numero di query eseguite:

$bookList = Book::orderBy('title', 'asc')->with('author')->get();

Ciò è perché l'informazione dell'autore sarà necessaria all'interno della view:

<td>{{ $book->author->lastname }}</td>

Nei metodi create e edit, che devono preparare la form per la creazione e la modifica, sarà necessario recuperare la lista degli autori disponibili che serviranno per popolare una select dando all'utente la possibilità di scegliere l'autore del libro corrente. Cosi facendo costruiamo un array associativo chiave/valore (in particolare id/lastname) da passare alla view:

$authorList = Author::orderBy('lastname', 'asc')->orderBy('firstname', 'asc')->lists('lastname', 'id');

per poi utilizzarlo nella view grazie a Form::select:

{!! Form::select('author_id', $authorList, null, ['class' => 'form-control'])!!}

Conclusioni

Biblios ha finalmente ha raggiunto un livello di utilizzabilità accettabile. Dal prossimo articolo introdurremo aspetti avanzati che ci permetteranno di arricchire l'applicazione.

I middleware in un'applicazione basata su Laravel

I middleware rappresentano dei componenti della applicazione Web che fungono da filtri per le chiamate HTTP in entrata. Possono essere usati per esempio per gestire l'autenticazione, ridirigendo l'utente ad una pagina di errore nel caso cercasse di accedere ad una risorsa privata, o per creare un sistema di logging integrato autonomo dai diversi controller. Laravel viene fornito con già un proprio set di middleware, ma è ovviamente possibile definirne di propri. Per creare un nuovo middleware, Laravel propone un apposito comando artisan:

php artisan make:middleware MyBrandNewMiddleware

I middleware, all'interno della struttura di cartelle di Laravel, vengono salvati in app/Http/Middleware. Il miglior modo per comprendere il meccanismo dei middleware è quello di immaginarli come dei diversi layer che vengono invocati in serie prima dello scatenamento del metodo del controller. In un qualsiasi punto di questi layer sarà possibile bloccare l'esecuzione standard o continuare con il flusso normale. Ciascun middleware deve implementare il metodo handle($request, Closure $next). All'interno di questo metodo sarà nostro onere quello di effettuare la logica di business desiderata prima di invocare la closure $next che equivale a continuare la catena di layers normalmente. Tramite i middleware è possibile intervenire sia prima che dopo l'invocazione del controller specifico. In base al momento in cui intervengono, i middleware prendono rispettivamente i nomi di before middleware e after middleware. Ecco due esempi:

class BeforeMiddleware
{
    public function handle($request, Closure $next)
    {
        // eseguo il codice custom del middleware
 
        return $next($request);
    }
}
 
class AfterMiddleware
{
    public function handle($request, Closure $next)
    {
        $response = $next($request);
 
        // eseguo il codice custom del middleware ($response conterrà  il corpo della risposta)
 
        return $response;
    }
}

Registrare i middleware

In base al contesto di utilizzo, possono esistere due diverse tipologie di middleware: i middleware globali e i middleware route-based. Nel primo caso si tratta di middleware che scattano per ogni chiamata HTTP verso l'applicazione, nel secondo caso è invece possibile filtrare la loro esecuzione in base alla rotta corrente. Per registrare dei middleware globali è necessario aggiungere il riferimento alla nostra classe middleware all'interno del file app/Http/kernel.php. Per registrare invece dei middleware basati sulle route è necessario prima registrarli ed assegnargli un nome (sempre all'interno di app/Http/Kernel.php) e successivamente utilizzare il loro nome all'interno della definizione delle rotte impiegando, per esempio, questa sintassi:

Route::get('/route/123', ['middleware' => 'auth', function() {
    // 
}]);

Nel caso fosse necessario, i middleware possono essere assegnati anche utilizzando dei gruppi di rotte:

Route::group(['middleware' => 'auth'], function () {
 
    Route::get('/route/123', function() {
 
    });
 
    [....]
 
});

Nell'ottica della non duplicazione del codice, è possibile definire middleware astratti e che possono modificare il loro comportamento in base a parametri definibili all'esterno del middleware stesso. Per implementare questa funzionalità è necessario inserire eventuali nuovi parametri nella definizione del metodo handle() e, nel momento della registrazione, impostare questi nuovi parametri. Supponendo di dover creare un middleware che comunichi via mail eventuali accessi all'applicazione, potremmo scrivere:

class EmailNotifierMiddleware
{
 
    public function handle($request, Closure $next, $recipient)
    {
        //invio mail a $recipient
 
        return $next($request);
    }
 
}
 
Route::get('/rotta/123', ['middleware' => 'emailnotifier:mail123@domain.it', function ($id) {
    //
}]);
 
Route::get('/rotta/456', ['middleware' => 'emailnotifier:mail345@domain.it', function ($id) {
    //
}]);

Service Provider in Laravel

I service provider rappresentano il cuore del framework, la parte interna che si occupa di registrare e avviare tutti i componenti presenti in una WebApp, come ad esempio i servizi, eventuali listener di eventi, i middleware e le rotte. All'interno del file config/app.php è possibile visualizzare l'elenco dei service provider configurati per la nostra applicazione. Ovviamente Laravel viene fornito con un set di service provider già configurati: alcuni di questi fanno parte del framework vero e proprio (quelli che appartengono al namespace Illuminate\) mentre altri, nonostante sono già automaticamente disponibili, sono fisicamente locati all'interno del nostro progetto (in app/Providers) e che appartengono al namespace custom definito (nel caso della nostra applicazione Biblios\). Tale differenza risiede nel fatto che questi ultimi presentano funzionalità che potrebbero essere personalizzate e quindi gli autori del framework hanno deciso di rendere eventuali modifiche più semplici. Nel dettaglio i service provider sono:

Service ProviderDescrizione
AppServiceProviderUn service provider vuoto, da utilizzare per piccole configurazioni.
AuthServiceProviderUn service provider utilizzato per definire il modello di autenticazione scelto.
EventServiceProviderUn service provider dedicato alla registrazione di eventuali eventi custom.
RouteServiceProviderUn service dedicato alla configurazione delle rotte al cui interno viene incluso il classico app/Http/Routes.php.

Creare service provider

Oltre ai service provider già disponibili in Laravel è ovviamente possibile registrarne altri. Il suggerimento è quello di crearne di nuovi solamente se i 4 descritti precedentemente non sono sufficienti. Per esempio per definire comportamenti personalizzati a livello di rotte o per creare eventi application-wide bisognerebbe utilizzare rispettivamente il RouteServiceProvider e l'EventServiceProvider. Per creare nuovi provider Laravel mette a disposizione il comando

php artisan make:provider NomeProvider

I provider così creati verranno posizionati insieme agli altri in app/Providers. Le classi create dovranno estendere Illuminate\Support\ServiceProvider e dovranno avere due metodi: register e boot. Ovviamente generando il file con make:provider tutto sarà già correttamente configurato. Il metodo register deve occuparsi esclusivamente della registrazione di nuovi componenti all'interno del service container. Non è consigliato scrivere frammenti di codice diversi dalla registrazione perché il metodo viene invocato prima del completamento del container e quindi eventuali servizi richiesti potrebbero non essere ancora pronti e istanziabili. Il metodo boot deve occuparsi di eventuali altre logiche da demandare al service provider, come per esempio configurare dei ViewComposer. All'interno del metodo boot, abbiamo la certezza che il service container è già stato inizializzato con successo e che quindi tutti i componenti sono già stati bindati con successo. Una volta definita la nostra classe è necessario registrare il service provider appena definito all'interno del file config/app.php nella sezione providers.

Biblios, servizi e provider

Il nostro progetto è rimasto fermo da qualche articolo. La situazione attuale è la seguente:

  • i modelli disponibili sono Book, Author e User. Tra Book e Author esiste una relazione uno a molti, mentre tra Book e User una relazione molti a molti (che permette di capire quali utenti hanno letto quali libri).
  • All'url /book è disponibile l'anagrafica dei libri, gestita dal BookController.
  • All'url /author è disponibile l'anagrafica degli autori, gestita dall'AuthorController.
  • Le rotte sono configurate tramite Route::resource

La parte disponibile potrebbe essere considerata una sezione di BackOffice (anche se non ancora resa privata). Occupiamoci ora di definire la parte publica a partire dalla home. La homepage presenterà diverse sezioni:

  • Gli ultimi 3 libri caricati in catalogo.
  • I 3 autori più prolifici.

Questi blocchi di contenuti dovranno essere riutilizzati in differenti pagine del sito e per questo motivo creeremo dei ViewComposer. Partiamo quindi creando un ViewComposerServiceProvider con il comando

php artisan make:provider ViewComposerServiceProvider

e lo registriamo in config/app.php. Nel provider registriamo due view composer:

public function boot()
{
    View::composer('shared.lastBook', \Biblios\Http\ViewComposers\LastBookViewComposer::class);
    View::composer('shared.prolificAuthor', \Biblios\Http\ViewComposers\ProlificAuthorViewComposer::class);
}

Creiamo quindi i due view composer, il primo che si occuperà di recuperare gli ultimi 3 libri e il secondo i 3 autori più prolifici.

class LastBookViewComposer {
 
    protected $lastBookList = [];
 
    public function __construct() {
        $this->lastBookList = Book::orderBy('created_at', 'desc')->limit(3)->get();
    }
 
    public function compose(View $view) {
        $view->with('lastBookList', $this->lastBookList);
    }
 
}
 
class ProlificAuthorViewComposer {
 
    protected $prolificAuthorList = [];
 
    public function __construct() {
        $this->prolificAuthorList = Author::with('books')->limit(3)->get()->sortBy(function($author) {
            return $author->books->count();
        })->reverse();
    }
 
    public function compose(View $view) {
        $view->with('prolificAuthorList', $this->prolificAuthorList);
    }
 
}

Passiamo quindi alla creazione di una nuova rotta per la homepage nella quale includeremo due viste che dovranno chiamarsi esattamente come i nomi registrati nel service provider: shared.lastBook e shared.prolificAuthor. Puntiamo quindi il nostro browser su:

localhost:8000

e dovremmo vedere le due liste.

Service Container in Laravel

Dipendency Injection

Alcuni dei pattern più importanti di Laravel, oltre al già introdotto Facade Pattern, sono l'Inversion of Control e la Dependecy Injection. L'Inversion of Control prevede un'inversione rispetto al classico modello di definizione delle librerie e dipendenze usabili da un particolare componente: non è piu il componente stesso a istanziare e gestire eventuali dipendenze, ma esisterà un componente globale a livello applicativo che fornirà le dipendenze. La Dependency Injection è un implementazione dell'Inversion of Control che prevede il passaggio di dipendenze attraverso i costruttori delle classi o tramite metodi setter dedicati. Una delle caratteristiche comuni a tutti i framework che implementano questi pattern, anche in linguaggi diversi da PHP, è il concetto di “Container”, un raccoglitore di componenti che si occupa di creare le relazioni di dipendenza tra di essi. Il Service Container di Laravel è appunto questo contenitore di classi dal quale sarà possibile utilizzare le dipendenze. Cerchiamo di capire meglio tramite un esempio contestualizzato alla nostra applicazione. Supponiamo di voler introdurre un sistema di prenotazione di libri che prevede delle logiche particolari di disponibilità; per esempio un libro potrebbe essere disponibile ad un determinato utente solamente se non c'è nessun altro utente in “coda” o se è la prima volta che lo richiede. A prescindere dall'implementazione, una buona pratica è quella di isolare questa logica in una classe dedicata, in modo da poterla riutilizzare in diversi controller. A questo scopo creiamo un servizio chiamato BookAvailabilityManager che implementa un particolare metodo isAvailable. Dato che l'argomento è la Dependency Injection non ci occuperemo della sua implementazione, diamo solamente per assodato che la classe esista e funzioni correttamente. Abbiamo però bisogno di utilizzare questo servizio all'interno del nostro BookController:

class BookController extends Controller {
 
    public function __construct(Biblios\Service\BookAvailabilityManager $bookAvailabilityManager) 
    {
        $this->bookAvailabilityManager = $bookAvailabilityManager;
    }
 
    public function showIfBookIsAvailable(Book $book) 
    {
        $isAvailable = $this->bookAvailabilityManager->isAvailable($book);
        [...]
    }
 
}

In questo piccolo esempio, abbiamo definito un costruttore per il nostro controller che si aspetta di ricevere come parametro il nostro BookAvailabilityManager. In questo caso sarà Laravel stesso ad accorgersi che abbiamo definito una dipendenza e a passare automaticamente un'istanza durante la creazione del BookController.

Binding

Per poter utilizzare la Dependecy Injection, oltre alla definizione inevitabile delle nostre classi è necessario creare dei binding all'interno del Service Container per istruire Laravel su quali componenti devono essere gestiti a livello di framework. La creazione dei binding è triviale e viene effettuata all'interno dei service provider (argomento del prossimo capitolo). Esistono tre tipologie di binding. La prima è il binding semplice che permette di definire una closure per istanziare una particolare classe. In questo caso, ogni volta che una classe richiederà il componente registrato la closure verrà invocata e il suo risultato verrà iniettato tramite costruttore:

$this->app->bind('Biblios\Service\BookAvailabilityManager', function ($app) {
    return new Biblios\Service\BookAvailabilityManager();
});

La seconda è il binding singleton, simile al binding semplice tranne per il fatto che la closure viene invocata solamente una volta e, ad eventuali richieste successive del componente, verrà iniettata sempre la stessa istanza:

$this->app->singleton('Biblios\Service\BookAvailabilityManager', function ($app) {
    return new Biblios\Service\BookAvailabilityManager();
});

Infine abbiamo il binding di un'istanza che, rispetto alle due precedenti modalità, permette di aggiungere al Service Container una particolare istanza di una classe e non una closure.

$bam = new Biblios\Service\BookAvailabilityManager();
$this->app->instance('Biblios\Service\BookAvailabilityManager', $bam);

Risolvere le dipendenze

Come abbiamo già visto nell'esempio iniziale, il miglior modo per risolvere una dipendenza è tramite il type-hinting nel costruttore del nostro componente, sia esso un controller, un middleware o un event listener. E' altresì possibile accedere ai componenti presenti nel Service Container tramite API dedicate sfruttando il metodo make:

$bam = $this->app->make('Biblios\Service\BookAvailabilityManager');

o utilizzare il Service Container come una array associativo PHP:

$bam = $this->app['Biblios\Service\BookAvailabilityManager'];

Anche secondo gli sviluppatori di Laravel, la modalità di inject tramite costruttore è da preferire a queste ultime.

Gestione Utenti in Laravel

Laravel presenta anche un modulo per la gestione dell'autenticazione e autorizzazione delle applicazioni Web. Il framework include automaticamente tra i modelli disponibili la classe User che rappresenta, grazie ad alcuni traits disponibili nel framework, l'entità che si occuperà di rappresentare gli utenti che potranno loggarsi al sito. All'interno del file config/auth.php saranno invece disponibili una serie di configurazioni per modificare il flusso di default del framework. Come impostazione predefinita Laravel sfrutta un'autenticazione basata su database e su Eloquent, ma è possibile modificarne il comportamento sia ignorando l'ORM sia non utilizzando del tutto il database. All'interno di questo e del prossimo articolo utilizzeremo il pattern standard offerto dal framework. Oltre al modello Laravel ci mette a disposizione anche due migration per il setup delle tabelle richieste (users e password_resets).

Controller out-of-the-box

All'interno della codebase di Laravel, sono presenti due controller dedicati alla gestione utente e disponibili nella cartella app/controllers/auth:

ControllerDescrizione
app\Http\Controllers\Auth\AuthControllerDedicato ad autenticazione e nuove registrazioni.
app\Http\Controllers\Auth\PasswordControllerDedicato al reset della password per utenti già registrati.

L'approccio offerto da Laravel allo sviluppatore è quello di rendere disponibili le principali logiche già implementate (comunque configurabili) ma di lasciare a lui la definizione delle rotte e le view (che dipendono strettamente dalle logiche di business). I metodi offerti dal framework sono questi:

MetodoDescrizione
AuthController@getLoginCreazione e visualizzazione della form di login, carica in automatico la view auth.login.
AuthController@postLoginValidazione e eventuale procedura di login.
AuthController@getLogoutLogout.
AuthController@getRegisterCreazione e visualizzazione della form di registrazione, carica in automatico la view auth.register.
AuthController@getLogoutValidazione e eventuale registrazione di un nuovo utente.
PasswordController@getEmailCreazione e visualizzazion del form “ricorda password”, carica in automatico la view auth.password.
PasswordController@postEmailValidazione e invio della mail contenente un link con token partendo dalla view emails.password.
Auth\PasswordController@getResetValidazione del token ricevuto via mail e visualizzazione del form di reset password, carica in automatico la view auth.reset.
Auth\PasswordController@postResetValidazione e modifica della password.

Configurazioni addizionali

Come anticipato, l'approccio del framework è quello di offrire logiche già implementate ma fortemente configurabili tramite property dedicate all'interno dell'AuthController. Quando un utente è autenticato con successo, il framework lo redirigerà all'URL /home ma nel caso questo URL non fosse compatibile con la nostra applicazione possiamo impostare la variabile $redirectPath. Viceversa, in caso di errore durante il processo di autenticazione verrà caricato l'URL /auth/login o eventualmente il valore della proprietà $loginPath. Oltre a poter configurare gli URL, all'interno di AuthController è possibile implementare nuovamente alcuni metodi, lasciati apposta a livello di applicazione:

MetodoDescrizione
validatorPermette di modificare le regole di validazione del form di registrazione.
createPermette di implementare una nuova logica per inserire il record nel database.

La facade Auth

Oltre ai controller descritti, Laravel presenta anche una facade dedicata a tutte le funzionalità legate all'autenticazione. Grazie al metodo attempt è possibile autenticare manualmente un utente passando email e password a prescindere dall'AuthController. Questo metodo è utile nel caso si creino link di autologin o possibilità di impersonificare un utente da parte di un amministratore. Oltre ad attempt è possibile utilizzare il metodo login che riceve direttamente un oggetto User come parametro o loginUsingId che richiede l'id dell'utente oppure once che permette di autenticare l'utente solamente per la richiesta successiva, utile in caso di API stateless che non sfruttino la sessione HTTP. Nel prossimo articolo implementeremo questo pattern all'interno di Biblios.

Login e registrazione utenti

Dopo aver appreso le basi su come Laravel gestisce autenticazioni e autorizzazioni, aggiungiamo ora una gestione utenti basilare ma efficace nella nostra applicazione, Biblios. Le specifiche da implementare sono: *maschera di login a partire da email e password; *pannello nella parte alta del sito che mostri il link alla pagina di login (se l'utente non è loggato) o i dati dell'utente loggato; *pagina di registrazione che utilizzi CAPTCHA per evitare i bot; *pagina privata dove definire alcuni libri come “preferiti”; *rendere private le pagine di anagrafica già create solo per gli amministratori, identificati da una colonna booleana su database. All'interno dell'articolo ci soffermeremo sugli aspetti specifici dell'argomento, non verrà invece proposto codice utile allo scopo ma non relativo alla “gestione utenti”. A corredo verranno comunque forniti i sorgenti funzionanti dell'applicazione in modo da poter testare il tutto e approfondire le parti non esplicitate nel testo.

Il login

Come primo passo per implementare il login dobbiamo creare una nuova migration che si occupi di modificare il database (per inserire la nuova colonna admin come da specifiche) e un nuovo seeder per popolare il database con degli utenti di test. Lanciamo quindi

php artisan make:migration update_users_admin

e

php artisan make:seed UserSeeder

e modifichiamo i file appena creati in base alle nostre esigenze. Una volta modificato e popolato il database possiamo concentrarci sulle prime due specifiche. Iniziamo dalle rotte necessarie che per il momento sono 3:

Route::get('/login', [
    'uses' => 'Auth\AuthController@getLogin',
    'as' => 'login'
]);
Route::post('/login', [
    'uses' => 'Auth\AuthController@postLogin',
    'as' => 'login.post'
]);
Route::group(['middleware' => 'auth'], function () {
    Route::get('/logout', [
        'uses' => 'Auth\AuthController@getLogout',
        'as' => 'logout'
    ]);
});

Da notare come la rotta di logout è inserita in un gruppo con un middleware (auth) in quanto è privata ed accessibile solo se l'utente è loggato. I controller utilizzati per le rotte sono quelli standard di Laravel. Dato che abbiamo configurato delle rotte personalizzate rispetto a quelle out-of-the box, dobbiamo configurare l'AuthController in modo che sappia dove redirigere l'utente. Aggiungiamo quindi queste righe sotto i traits:

protected $redirectPath = '/';
protected $loginPath = '/login';

La prima regola farà si che l'utente una volta loggato venga rediretto alla home, la seconda lo porterà al login nel caso cercasse di accedere a pagine riservate senza permessi. Ora possiamo concentrarci sulle view. Gli interventi da fare sono due, implementare la pagina login.blade.php in resources/view/auth e modificare layout/biblios.blade.php per aggiungere il pannello nell'header che verrà mostrato in tutte le pagine.

<div class="row">
    <div class="col-md-6 pull-right">
        @if(Auth::check())
            Benvenuto <b>{{ Auth::user()->name }}</b> <a href="{{ route('logout') }}">Logout</a>
        @else
            <a href="{{ route('login') }}">Login</a>
        @endif
    </div>
</div

Se tutto è stato implementato correttamente, dopo queste modifiche saremo in grado di loggarci e di fare logout. Un test ulteriore è provare ad accedere a /logout da utenti anonimi e verificare che l'applicazione ci rimandi al login.

Registrazione di nuovi utenti

Un login è poco funzionale se non è possibile registrarsi. Implementiamo quindi la terza specifica e aggiungiamo due rotte mappate verso i controller standard di Laravel:

Route::get('/register', [
    'uses' => 'Auth\AuthController@getRegister',
    'as' => 'register'
]);
 
Route::post('/register', [
    'uses' => 'Auth\AuthController@postRegister',
    'as' => 'register.post'
]);

Per l'implementazione del CAPTCHA dobbiamo affidarci ad una libreria. Per gli esempi ho utilizzato captcha che oltre ad essere di buona fattura è disponibile come dipendenza composer. Installiamola con:

composer require mews/captcha

e pubblichiamone la configurazione con:

php artisan vendor:publish

verrà creato il file config/captcha.php. Dopo aver creato il file resources/view/register.blade.php (che sarà disponibile nei sorgenti), configuriamo l'AuthController in base alle nostre esigenze. Per far questo dobbiamo modificare le regole di validazione in quanto è disponibile un nuovo campo, il CAPTCHA che richiede una validazione e il metodo di costruzione per impostare su false il nuovo campo admin (passaggio non strettamente obbligatorio in quanto i campi boolean sono falsi di default).

protected function validator(array $data)
{
    return Validator::make($data, [
        'name' => 'required|max:255',
        'email' => 'required|email|max:255|unique:users',
        'password' => 'required|confirmed|min:6',
        'captcha' => 'required|captcha'
    ]);
}
 
protected function create(array $data)
{
    return User::create([
        'name' => $data['name'],
        'email' => $data['email'],
        'password' => bcrypt($data['password']),
        'admin' => false
    ]);
}

Facciamo quindi un test di registrazione alla fine del quale dovremmo ritrovarci nella home ma con un nuovo utente archiviato su database.

Pagina privata, amministratori e anagrafica

Accesso alla pagina privata

Manteniamo lo stesso pattern di sviluppo utilizzato per il capitolo precedente e occupiamoci ora delle rotte:

Route::group(['middleware' => 'auth'], function () {
    Route::get('/my-page', [
        'uses' => 'FrontendController@getPrivatePage',
        'as' => 'private-page'
    ]);
    Route::get('/my-page/{bookId}', [
        'uses' => 'FrontendController@addFavouriteBook',
        'as' => 'favourite-book'
    ]);
});

Dedichiamoci quindi ai due nuovi metodi nel FrontendController:

public function getPrivatePage()
{
    return View::make('private');
}
 
public function addFavouriteBook($bookId)
{
    Auth::user()->books()->attach(Book::findOrFail($bookId));
    return redirect(route('private-page'));
}

Il primo metodo (getPrivatePage) non fa altro che mostrare una view dedicata, mentre il secondo metodo si occupa di ottenere l'oggetto Book dal database e di associarlo all'elenco dei libri preferiti dall'utente corrente. Nelle view dobbiamo implementare due aspetti. Il primo è la creazione della pagina privata:

<div class="row">
    <div class="col-md-6">
 
        @include('shared.lastBook')
 
    </div>
    <div class="col-md-6">
 
        <h3>I miei libri preferiti</h3>
 
        <ul>
            @foreach(Auth::user()->books as $book)
                <li>{{ $book->title }} ({{ $book->pivot->created_at }})</li>
            @endforeach
        </ul>
 
    </div>
</div>

Il secondo è la modifica del view composer lastBook per mostrare un link dedicato all'aggiunta del libro tra i preferiti:

<ul>
    @foreach($lastBookList as $book)
        <li>
            {{ $book->title }}
            @if(Auth::check())
                <a href="{{ route('favourite-book', [ 'bookId' => $book->id ]) }}">
                    Aggiungi
                </a>
            @endif
        </li>
    @endforeach
</ul

In questo caso, se tutto è stato implementato correttamente, potremmo aggiungere dei libri tra i preferiti. Ulteriori implementazioni potrebbero essere quelle di evitare l'inserimento di “doppioni” e implementare l'operazione inversa, ovvero l'eliminazione di un libro dai preferiti.

Amministratori e anagrafica contenuti

L'ultima specifica che manca è quella di negare l'accesso alle pagine di anagrafica per gli utenti non amministratori. A livello di configurazione di modelli abbiamo già svolto il lavoro aggiungendo una nuova colonna nel database. L'operazione rimanente è a livello di configurazione di rotte, per aggiungere un middleware dedicato a questo controllo personalizzato per le rotte del backoffice. Creiamo quindi un nuovo middleware con il comando:

php artisan make:middleware Admin

e aggiungiamolo all'interno di app/Http/Kernel.php tra i middleware registrati. Implementiamo quindi la sua logica basandoci sul middleware Authenticate già presente:

public function handle($request, Closure $next)
{
    if (!$this->auth->user()->admin) {
        if ($request->ajax()) {
            return response('Unauthorized.', 401);
        } else {
            return redirect()->guest('login');
        }
    }
 
    return $next($request);
}

Come ultima operazione modifichiamo il file delle rotte, incapsulando le rotte di anagrafica in un nuovo gruppo dedicato a questo controllo:

Route::group(['middleware' => 'admin'], function () {
    Route::resource('author', 'AuthorController');
    Route::resource('book', 'BookController');
});

Se tutto è stato implementato correttamente, ora solo gli utenti amministratori potranno accedere alle rotte /author e /book.

Localizzazione e pluralizzazione

Localizzazione

La cartella resources/lang contiene le stringhe localizzate utili all'internazionalizzazione delle applicazioni Web. All'interno di questa cartella sono presenti diverse sotto cartelle, ognuna per ogni linguaggio supportato. Dentro ciascuna di queste cartelle sarà nostro compito creare diversi file, che fungono da raccolte logiche di etichette localizzate. Ciascuno di questi file si occuperà di definire un hash chiave/valore. Di default Laravel inizializza una applicazione con una serie di file dedicati alla lingua inglese: en. Il linguaggio di default di una applicazione è definito in config/app.php ma ovviamente può essere modificato tramite il metodo setLocale della facade App. Sempre dentro app.php è possibile configurare un linguaggio di fallback, utilizzato nel caso manchi qualche etichetta del linguaggio corrente. Per poter utilizzare le etichette localizzate all'interno dei componenti software, il framework mette a disposizione un helper: trans. Il metodo riceve in ingresso una stringa che identifica sia il contesto sia il nome della label da stampare. Per esempio la dicitura:

{{ trans('auth.failed') }}

stamperà il valore dell'etichetta failed all'interno del file auth.php. L'helper può ricevere anche un ulteriore parametro, una mappa di valori da sostituire nella stringa corrente. Supponendo di avere una label del tipo:

'required' => 'The :attribute field is required.'

sarà possibile scrivere

{{ trans('validation.required', [ 'attribute' => 'username ]) }}

per avere un messaggio personalizzato in base all'attributo corrente.

Pluralizzazione

Un ulteriore feature del componente dedicato alle localizzazioni è la pluralizzazione. Tutte le lingue del mondo presentano regole differenti per creare le versioni plurali dei sostantivi e per questo non è sempre possibile automatizzare questo processo. Con un particolare sintassi e un metodo ad hoc, è possibile configurare Laravel impostando diverse frasi in base al numero di elementi. Facciamo un esempio:

'utenti' => '{0}Sei l'unico utente|{1,3} Siete in quattro gatti|{4,10} Un bel gruppetto|{11, Inf}Troppo casino'
 
trans_choice('frontend.utenti', 10)

In questo caso, grazie a trans_choice e al secondo parametro passato, possiamo ottenere un messaggio personalizzato anche in base alla quantità definita; nel nostro caso il numero 10 permette di ottenere la stringa “Un bel gruppetto”.

Laravel e la validazione dei dati

Validazione e controller

Il pattern MVC di Laravel prevede che la validazione dei dati deve essere eseguita all'interno dei controller in quanto componenti dedicati all'integrazione Web dell'applicazione. Per questo scopo, il framework, fornisce un trait, ValidatesRequest, che implementa un metodo, validate, che appunto facilita la validazione di request HTTP. Il metodo accetta la richiesta HTTP corrente e una serie di regole, in automatico si occupa di rispondere all'utente con una notifica di errore in caso di dati non conformi. All'interno della nostra applicazione demo abbiamo già utilizzato questa modalità di validazione all'interno dei nostri controller. Riporto un esempio per comodità preso dall'AuthorController:

public function store(Request $request) {
    $this->validate($request, [
        'firstname' => 'required|min:3|max:40',
        'lastname' => 'required|min:3|max:40'
    ]);
    [....]
}

In questo caso le regole sono semplici: firstname e lastname sono campi obbligatori di lunghezza compresa tra 3 e 40 caratteri. L'oggetto $request invece viene reso disponibile da Laravel stesso, tramite dependency injection, grazie alla definizione del tipo dell'oggetto nella firma del metodo. La definizione delle regole però non è l'unica attività da svolgere; è necessario anche notificare all'utente gli errori che il sistema ha identificato. In caso di errori il framework redirige l'utente sulla pagina precedente (nella quale solitamente è presente un form) creando una variabile flash contenente gli errori. Le variabili flash sono speciali variabili che vengono salvate nella sessione HTTP ma che hanno durata limitata ad una solo richiesta e vengono utilizzate esclusivamente per un passaggio di dati tra due pagine. Per questo motivo, all'interno della nostra view, sarà disponibile una variabile di nome $errors che sarà un'istanza di Illuminate\Support\MessageBag. Ecco come vengono mostrati gli errori di uno specifico campo all'interno di author/form.blade.php:

<div class="form-group {{ $errors->has('firstname') ? 'has-error' : '' }}">
    {!! Form::label('firstname', 'Firstname') !!}
    {!! Form::text('firstname', null, ['class' => 'form-control']) !!}
    @foreach($errors->get('firstname') as $error)
        <span class="help-block">{{ $error }}</span>
    @endforeach
</div>

Altre modalità di validazione

La modalità implementata in Biblios è la più semplice e quella offerta di default da Laravel. Nel caso ci fosse bisogno di un maggior controllo sul flusso di approvazione dei dati è comunque possibile configurare la validazione ad un livello più basso. La prima modalità è quella di creare manualmente il validatore senza utilizzare il trait ValidatesRequest. In questo modo le righe da scriver sono di più ma è possibile personalizzare il comportamento nel caso di errori:

public function store(Request $request)
{
    $validator = Validator::make($request->all(), [
        'firstname' => 'required|min:3|max:40',
        'lastname' => 'required|min:3|max:40'
    ]);
 
    if ($validator->fails()) {
        return redirect('author/form')
                    ->withErrors($validator)
                    ->withInput();
    }
 
    [....]
}

Tale modalità, che permette una maggiore riusabilità del codice, prevede la creazione di un oggetto Request personalizzato. Laravel infatti permette di iniettare all'interno dei metodi dei controller non solo oggetti standard ma anche personalizzati. E' infatti possibile definire tipologie di richieste custom, con delle proprie caratteristiche e riutilizzarle anche in controller differenti. Eventuali oggetti Request vengono salvati all'interno della cartella app/Http/Request ed è presente un comando per la creazione automatica di questi file:

php artisan make:request

Le request create in questo modo presentano alcuni metodi già definiti ma senza comportamento specifico. Il metodo da implementare è rules che deve restituire l'elenco delle regole attive per la richiesta. Una volta definite sarà possibile utilizzare l'oggetto sfruttando la dependency injection nei controller. La validazione scatterà a priori e il codice del controller verrà eseguito solamente se la validazione ha dato esito positivo.

Messaggi di errore

I messaggi mostrati sono etichette di testo localizzate gestite dal motore di localizzazione di Laravel. In automatico Laravel utilizza i messaggi contenuti nel file resources/lang/validation.php me è possibile personalizzarli tramite due approcci: 1.modificando i file contenenti le risorse per creare messaggi comuni a tutta l'applicazione; 1.passando un nuovo oggetto al metodo statico Validator::make per creare messaggi dedicati alla singola istanza del validatore. Dato che Laravel presenta solamente messaggi tradotti in inglese, può essere comodo aggiungere tra le dipendenze questo progetto che presenta messaggi di validazione già localizzati in 52 diversi idiomi.

Regole di validazione standard e custom

Laravel presenta un set abbastanza ricco di regole out-of-the-box, è però possibile implementare logiche e regole personalizzate di validazione all'interno del metodo boot di un service provider in modo che il framework le carichi all'avvio. Un'eventuale regola (canDrive) che controlla che l'età sia maggiore del minimo legale per avere la patente potrebbe essere:

[...]
public function boot()
{
    Validator::extend('canDrive', function($attribute, $value, $parameters, $validator) {
        return $value >= 18;
    });
}
[...]

Per utilizzala si dovrà richiamarla come se fosse una normale regola Laravel.

Laravel: gli ultimi aggiornamenti

php/laravel.1493592641.txt.gz · Ultima modifica: 2017/04/30 22:50 da apressato
Torna su
CC Attribution-Share Alike 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0