Laravel – primi passi: i Model

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

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

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

Accesso diretto al database dal Controller (senza model)

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

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

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

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

use Illuminate\Support\Facades\DB;

Accesso ai dati attraverso il model

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

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

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

use Illuminate\Database\Eloquent\Model;

class Company extends Model
{
}

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

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

Passaggio dei dati dal model alla view

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

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

  • index
  • edit
  • create
  • ecc…

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

route:list con resource
route:list con resource

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

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

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

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

@extends('layouts.app')

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

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

Laravel: company index page
Laravel: company index page

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

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

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

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

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

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

View per l’editazione del record

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

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

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

Per questa view abbiamo bisogno di tre cose:

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

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

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

@extends('layouts.app')

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

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

@endsection

Il risutato è questo:

Laravel Model: company edit
Laravel Model: company edit

Riferimenti

Laravel – primi passi: i Controller

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

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

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

Ma vediamo prima di tutto come creare un nuovo controller.

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

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

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

$ php artisan make:controller CompanyController

Un piccolo inciso:

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

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

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

$ php artisan make:controller --resource CompanyController

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

namespace App\Http\Controllers;

use Illuminate\Http\Request;

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

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

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

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

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

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

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

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

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

e definiamo la rotta così

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

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

il risultato sarà

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

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

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

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

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

Il risultato è il seguente:

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

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

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

Basta creare una route di tipo resource:

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

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

$ php artisan route:list

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

route:list con resource
route:list con resource

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

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

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

Riferimenti

Laravel: convention over configuration

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

Un esempio rende tutto chiaro.

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

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

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

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

Riferimenti

Git: caricare un nuovo progetto su Bitbucket

Atlassian_Bitbucket_Logo
Atlassian_Bitbucket_Logo
git
git

Qui di seguito scrivo una breve guida passo passo per caricare su Bitbucket un repository Git e utilizzarlo come copia di riferimento nello sviluppo. Bitbucket consente di ospitare repository remoti privati con alcune limitazioni.

Git: creare il repository in locale

Come primo passaggio occorre inzializzare il repository git in locale:

$ cd /my/proj/repo/
$ git init
$ git add *
$ git commit -m "Importazione iniziale"

Qui è neceessario individuare il nome che git ha attribuito al branch principale per poterlo replicare in remoto:

$ git branch
* master

La versione di Git che ho a bordo è

$ git --version
git version 2.30.2

Bitbucket: creare il repository in remoto

Dopodiché bisogna creare un nuovo repository su Bitbucket, un repository vuoto in cui il branch principale divrà chiamarsi master (Bitbucket da’ l’opzione di chiamare come si vuole il branch principale, ma è bene fare molta attenzione su questo punto!).

Il progetto Laravel contiene già i file Readme.md e .gitignore quindi non è necessario generarli lato Bitbucket.

In “impostazioni avanzate” della maschera di creazione del repository Bitbucket possiamo scegliere il linguaggio con cui stiamo sviluppando, i questo caso PHP.

Quindi colleghiamo il repo locale con quello remoto con il comando:

$ git remote add origin git@bitbucket.org:mxaos/complex.git

Qui si è scelto di chiare “origin” il repository remoto come si fa spesso. Ma lo possiamo chiamare come vogliamo. Ora si può sparare sù il repository:

$ git push -u origin master

Se il risultato è il seguente:

sign_and_send_pubkey: signing failed for RSA "/home/marcob/.ssh/id_rsa" from agent: agent refused operation
git@bitbucket.org: Permission denied (publickey).
fatal: Impossibile leggere dal repository remoto.

Assicurati di disporre dei privilegi d'accesso corretti
e che il repository esista.

c’è un problema con l’autenticazione con SSH. Vedere questo articolo per risolvere il problema.

In sostanza occorre avviare l’agente ssh (se lo si deve fare ogni volta, è preferibile aggiungerlo negli script di avvio di Linux).

Infatti il risultato dopo l’esportazione delle variabili di ambiente di ssh-agent è il seguente

$ git push -u origin master
Enter passphrase for key '/home/marcob/.ssh/id_rsa': 
Enumerazione degli oggetti in corso: 109, fatto.
Conteggio degli oggetti in corso: 100% (109/109), fatto.
Compressione delta in corso, uso fino a 4 thread
Compressione oggetti in corso: 100% (92/92), fatto.
Scrittura degli oggetti in corso: 100% (109/109), 226.50 KiB | 4.36 MiB/s, fatto.
109 oggetti totali (7 delta), 0 riutilizzati (0 delta), 0 riutilizzati nel file pack
remote: Resolving deltas: 100% (7/7), done.
To bitbucket.org:mxaos/complex.git
 * [new branch]      master -> master
Branch 'master' impostato per tracciare il branch remoto 'master' da 'origin'.

La situzione in remoto ora è la seguente

Git: caricare un nuovo repository su Bitbucket da progetto locale
Git: caricare un nuovo repository su Bitbucket da progetto locale

Riferimenti

SSH: problema di autenticazione su Bitbucket

ssh
ssh

Ho un nuovo repository su Bitbucket. Ho generato una chiave con ssh-keygen e ho caricato la chiave pubblica su Bitbucket.

Tuttavia il comando seguente mi va in errore:

$ git pull remote master
sign_and_send_pubkey: signing failed for RSA "/home/marcob/.ssh/id_rsa" from agent: agent refused operation
git@bitbucket.org: Permission denied (publickey).
fatal: Impossibile leggere dal repository remoto.

Assicurati di disporre dei privilegi d'accesso corretti
e che il repository esista.

Il problema è che, dopo aver generato la chiave, mi sono dimenticato di aggiungerla al keyring utlizzato dall’agent ssh. Quindi è necessario lanciare questo comando:

$ ssh-add
Enter passphrase for /home/marcob/.ssh/id_rsa: 
Identity added: /home/marcob/.ssh/id_rsa (marcob@jsbach)

Alla fine si può verificare che la chiave è stata aggiunta

$ ssh-add -l
4096 SHA256:dCoHG9JO4UicfP5ct55GGTIN6U0bDzNrnUj70zbdkSc marcob@jsbach (RSA)

Due parole sulla autenticazione a chiave pubblica SSH

Posso effettuare un’autenticazione su un server remoto con questi ingredienti:

  1. Un repository locale in cui risiede una coppia di chiavi RSA (generate con l’utility ssh-keygen)
  2. Un agente locale (ssh-agent) che gestisce le coppie RSA generate in locale
  3. Un server in cui gira un demone OpenSSH
  4. Un agente remoto che colloquia con l’agente locale

Inoltre, il protocollo SSH implementa l’inoltro dell’agente, un meccanismo per cui un client SSH consente a un server SSH di utilizzare l’agente ssh locale sul server a cui l’utente accede, come se fosse locale.

Quando l’utente contatta il client SSH sul server, il client tenterà di contattare l’agente implementato dal server e il server inoltra la richiesta al client che ha originariamente contattato il server, che la inoltra ulteriormente all’agente locale. In questo modo, ssh-agent e agent forwarding implementano il single sign-on che può progredire in modo transitivo.

Comunque: se oggi funziona e dopo un po’ non funziona più, è perché dev’essere riavviato l’ssh-agent.

SSH agent

ssh-agent è in sostanza un portachiavi. Un programma per contenere chiavi private usate per l’autenticazione a chiave pubblica.

Attraverso l’uso di variabili d’ambiente, l’agente può essere individuato da ssh e utilizzato automaticamente per l’autenticazione durante la registrazione in altre macchine.

L’avvio del portachiavi con inizializzazione delle variabili di ambiente viene fatta con il comando

$ eval `ssh-agent -s`

Per esempio le variabili possono assumere valori come questi:

SSH_AUTH_SOCK=/tmp/ssh-9OGf5JBFxGy6/agent.20980; export SSH_AUTH_SOCK;
SSH_AGENT_PID=20981; export SSH_AGENT_PID;
echo Agent pid 20981;

Qui le variabili inizializzate sono due: SSH_AUTH_SOCK e SSH_AGENT_PID.

SSH può accedere al portachiavi utilizzando queste due informazioni.

Se l’agente vi chiede nuovamente la passphrase

Vuol dire che non avete aggiunto la chiave al keyring; anzi, probabilmente non è colpa vostra: lo avete fatto ma l’accesso al portachiavi è come sempre a tempo: dopo un po’ di tempo dovete riaprirlo a mano. Potete però decidere per quanto tenerlo aperto con questo comando:

$ ssh-add -t 1h30m

per esempio così rimane aperto per un’ora e mezza. Ricordatevi comunque che è bene che il portachiavi si chiuda dopo un po’.

Comunque è sufficiente ridigitare:

$ ssh-add

Per verificare che sono state aggiunte le chiavi

$ ssh-add -l
4096 SHA256:dCoHG9JO4UicfP5ct55GGTIN6U0bDzNrnUj70zbdkSc marcob@jsbach (RSA)

Ricapitolando

Prima dell’apertura del portachiavi questa è la risposta del server Git da cui voglio scaricare gli aggiornamenti:

$ git pull origin master 
sign_and_send_pubkey: signing failed for RSA "/home/marcob/.ssh/id_rsa" from agent: agent refused operation
git@bitbucket.org: Permission denied (publickey).
fatal: Impossibile leggere dal repository remoto.

Assicurati di disporre dei privilegi d'accesso corretti
e che il repository esista.

Quindi riapro il portachiavi:

$ eval `ssh-agent -s`
Agent pid 170012

Ora se provo a ridare il comando non avrò più l’errore ma mi verrà chesta la passphrase della mia chiave privata:

$ git pull origin master 
Enter passphrase for key '/home/marcob/.ssh/id_rsa': 

Se voglio anche non dover ridigitare la password, aggiungo la mia chiave privata al portachiavi (devo però digitare la passphrase almeno questa volta)

$ ssh-add
Enter passphrase for /home/marcob/.ssh/id_rsa: 
Identity added: /home/marcob/.ssh/id_rsa (marcob@jsbach)

Per le successive invocazioni con il Git server remoto non mi verrà più chiesta la password (fino allo scadere del timeout del portachiavi):

$ git pull origin master
Da bitbucket.org:mxaos/logisticmapper
 * branch            master     -> FETCH_HEAD
Già aggiornato.

Riferimenti SSH

Laravel – primi passi – le rotte (routes)

Laravel: cominciamo con le rotte (routes)
Laravel: cominciamo con le rotte (routes)

Routes: credo che il modo più semplice di imparare come funziona il framework MVC Laravel sia quello di partire con le rotte. Rotte proprio come le linee ideali seguite da una nave.

Almeno questa è la mia esperienza.

Cosa sono le rotte (routes)?

Cosa sono le rotte? In inglese routes, sono gli URI (Uniform Resource Identifier), i link che vengono generati dall’applicazione, quelli che compaiono sulla barra degli indirizzi del browser:

Routes: sono gli url nella barra degli indirizzi
Routes: sono gli URI nella barra degli indirizzi

Il principale URI generato dall’applicazione è la home page, nel mio esempio:

http://www.complex.local/

È un’applicazione di esempio locale per la quale ho configurato un virtual host Apache.

Per vedere come viene servita questa rotta (che in sostanza è risorsa di più alto livello, la radice e si indica con /) bisogna fare riferimento al file <APP>/routes/web.php.

Attenzione: sto usando la versione 8.73.1 di Laravel e, rispetto al passato, l’organizzazione delle cartelle è leggermente cambiata. Con <APP> intendo la cartella radice dell’applicazione (nel mio caso il nome della cartella è /complex. Al suo interno c’è tutta l’organizzazione delle cartelle di Laravel, tra cui una che si chiama /app (che non è <APP>, bensì la prima sottocartella di questa, in ordine alfabetico).
Nella cartella /app c’è la maggior parte del materiale che utilizzeremo (i model e i controller per esempio).
Un’altra cartella importante è la cartella /database dove troveremo tutte le migrazioni.
Infine , nella cartella /resources/views ci sono le viste e, nella separata cartella /routes, le rotte.

Ebbene, ecco il contenuto del file <APP>/routes/web.php:

<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

Questo programma utilizza la classe di Laravel Route e in particolare il suo metodo get(), che è definito con una closure che non è altro che una funzione definita sul posto, detta più propriamente funzione anonima perché è senza nome. Questa closure ritorna la funzione view() a cui passo l’argomento 'welcome'.

Nota: view() è una funzione, non un metodo. Infatti non è definita all’interno di una classe ma all’interno di un file, helpers.php, situato sotto <APP>/vendor/laravel/framework/src/Illuminate/Foundation.

Il primo importante parametro del metodo get() è proprio la rotta da servire. In questo caso la ‘/’, la home page.

Il secondo parametro altrettanto importante è cosa deve fare Laravel quando dal browser arriva la richiesta di servire la rotta indicata. In questo caso ciò che deve fare Laravel è scritto dentro alla closure, e deve ritornare la view di nome ‘welcome’.

Cos’è questa view 'welcome'? È un file PHP scritto in Blade che si trova sotto <APP>/resources/views/welcome.blade.php. Quindi ogni volta che di decide il nome di una nuova view deve essere necessariamente associata ad un nuovo file all’interno della cartella <APP>/resources/views/.

Per esempio, se dobbiamo servire una rotta return view('edit'); ci dovrà essere un form HTML scritto dentro al file <APP>/resources/views/edit.blade.php.

Quindi in generale:

Route::get([URL], function () {
    return view([view file name]);
});

Ora, la view di home generata automaticamente da Laravel è piuttosto complessa:

laravel 8.73.1 standard home
laravel 8.73.1 standard home

Facciamone una più semplice:

$ cd resources/views/
$ cp welcome.blade.php welcome.blade.ORIG.php

e modifichiamo a piacere l’orginale (welcome.blade.php):

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Laravel</title>

        <!-- Fonts -->
        <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">

        <!-- Styles -->
        <style>
            /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;...}
        </style>

        <style>
            body {
                font-family: 'Nunito', sans-serif;
            }
        </style>
    </head>
    <body class="antialiased">
        <div class="relative flex items-top justify-center min-h-screen bg-gray-100 dark:bg-gray-900 sm:items-center py-4 sm:pt-0">
            <h1>Buongiorno Laravel!</h1>
            <p>Questa è la home page</p>
        </div>
    </body>
</html>

Ho lasciato un po’ di “codice” html/css per fare vedere lo scheletro della pagina, come vengono caricati gli stili, come si usa il linguaggio di templating Blade (es. str_replace()) – per vedere come si possano iniettare piccoli frammenti di codice per automatizzare la pagina – e Bootstrap con la definizione delle classi.

Ma proprio il minimo indispensabile.

Il cuore della view è:

<h1>Buongiorno Laravel!</h1>
<p>Questa è la home page</p>

Il risultato è un po’ deludente ma siamo risuciti a servire la nostra prima rotta

laravel home modificata
laravel home modificata

Routes: il metodo statico Route::get()

Ho glissato su un aspetto tecnico piuttosto importante, il frammento di programma che chiama la view è scritto così:

<?php
Route::get($path, <closure>)

la scrittura Route::get() è l’invocazione statica di un metodo di una classe. Qui spiego meglio cosa vuol dire.

Se non definisco una rotta, non la posso chiamare dal browser. Per esempio se provo ad invocare

http://www.complex.local/esempio

il risultato è un errore 404

Route errore 404
Route errore 404

Se però definisco la rotta:

Route::get('/esempio', function () {
    return 'view di esempio';
});

la vista che appare è la seguente

Route: view di esempio
Route: view di esempio

Possiamo anche passare parametri nell’URL e intercettarli all’interno dei nostri programmi per farne ciò che vogliamo. Ad esempio se definiamo questa rotta nel file /routes/web.php:

Route::get('/company/{id}', function ($id) {
    return 'Company '.$id;
});

Otteniamo

Route per passare un parametro
Route per passare un parametro

Posso passare anche più parametri nel modo seguente (attenzione all’ordine in cui li scrivo e li nomino!):

Route::get('/company/{id}/{type_id}', function ($id, $type_id) {
    return 'Company '.$id. ' is of type '.$type_id;
});

Il risultato è il seguente

Route per passare più parametri
Route per passare più parametri

Questo esempio che segue illustra la possibilità di dare un nome, una etichetta, ad una rotta.

Route::get('/user/example', array('as' => 'user.home', function() {
    $uri = route('user.home');
    print 'The URI is '.$uri;
}));

Il risultato è questo che per ora ci dice poco:

Route name
Route name

Tutte le rotte che definiamo le possiamo tenere sotto controllo con artisan:

Route: lista delle rotte
Route: lista delle rotte

L’ultima lista dell’elenco è quella a cui abbiamo anche attriibuto un nome. In pratica lo potremo usare così:

<a href="route('user.home')">User home</a>

e laravel ci porterà a http://www.complex.local/user/example.

Ricordatevi di fare in modo che apache possa scrivere il file <APP>/storage/logs/laravel.log:

 $ sudo chmod 777 storage/logs/laravel.log 
[sudo] password di marcob: 
$

o risulterà un errore.

Ulteriori modi di utilizzare le rotte è chiamando i metodi di un controller invece di una funzione anonima. Ma lo vediamo la prossima puntata.

Riferimenti

Laravel – primi passi – MVC

Laravel logo
Laravel logo

Laravel primi passi: ispirandomi alle lezioni di Edwin Diaz ripropongo qui un approccio smooth.

Laravel è un framework per applicazioni scritte PHP che implementa un’architettura MVC.

Framework sta per infrastruttura software fatta di file, classi, metodi e proprietà, che aiutano a sviluppare un’applicazione.

È in se stessa un’applicazione, ma sapendola configurare e personalizzare, con essa possiamo fare ciò vogliamo.

Molte operazioni saranno quasi gratis, come le operazioni su un database, ma occorrerà comunque scrivere parecchio per realizzare ciò che vogliamo. Il fatto è che utiizzando il framework siamo costretti a sviluppare in modo ordinato e coerente e possiamo tenere sotto controllo gli errori, testare il software e documentarlo.

Un altro aspetto positivo dell’adozione di un paradigma MVC standard è che potrai essere affiancato da altri programmatori che non dovranno imparare come tu scrivi il software, che è il peggior problema quando si mettono le mani in codice scritto da altri: ognuno ha il suo stile e si fa il suo piccolo framework.

Anche qui ci sarà una curva di apprendimento ma lo standard è molto più robusto, scalabile, testato e documentato del piccolo framework personale

Laravel: architettura MVC

L’arhitettura MVC è un design pattern, cioè un paradigma progettuale, un modo di progettare, in cui il funzionamento di un software viene pensato come composto da tre componenti:

  • M per Model: il modello è la rappresentazione dei dati all’interno dell’applicazione. I dati risiedono in un database ma, nel contesto di una programmmazione ad oggetti, anche loro all’interno del software, devono venire rappresentati come degli oggetti del linguaggio che si sta usando. Per questo il modello in sostanza è implementato da un’architettura che si chiama ORM, Object Relationship Manager, ed è la parte di Laravel che si occupa di interagire con il database, come dice il nome, mappando il database sugli oggetti PHP. Nella stragrande maggioranza dei casi si può dire che ad ogni tabella del database corrisponde una classe del modello.
    ORM ce ne sono tanti, ogni linguaggio ha il suo. Per esempio un ORM di Java è Hibernate. L’ORM di PHP adottato da Laravel si chiama Eloquent. Con Eloquent possiamo fare le più semplici interrogazioni e query con join senza ricorrere alla scrittura diretta di istruzioni SQL. Possiamo anche creare e distruggere oggetti database senza ricorre al DDL (il Data Definition Language, per intederci le istruzioni di tipo CREATE TABLE) del DBMS . È comunque abbastanza flessibile da consentire anche di scrivere direttamente in SQL query troppo complicate rimanendo nel framework.
  • V per View: è la parte visuale dell’applicazione, l’html prodotto dall’applicazione e la gestione dell’interazione con il browser. Il fatto che l’interfaccia debba essere sollevata da interazioni dirette con il database dovrebbe essere chiara a chiunque abbia sperimentato un po’ di programazione old style che mescolava dentro ogni singola pagina: HTML, CSS, PHP, SQL, Javascript… Un vero incubo!
    Anche per le view esistono plugin per semplificare ulteriormente la produzione: per esempio il framework Blade consente di progettare pagine che acquisiscono dati dal Model attraverso il Controller e fanno operazioni di tipo ciclo o test o semplicemente output delle variabili nell’HTML.
    Utilizzando anche il tool HTML/CSS Bootstrap possiamo progettare in modo rapido e responsive le nostre pagine senza impazzire con il CSS al cambiare della UI.
  • C per Controller: devo dire che per me è stato il componente più difficile da imparare ma quando ho capito cos’è, si è semplificato tutto di brutto. Viene definito come il man in the middle, e un po’ di verità c’è. Ma la sostanza è che il controller è l’applicazione. È in assoluto la parte più importante del pattern MVC, quella in cui scarichiamo la maggior parte della nostra creatività. Ciò che vogliamo che l’applicazione faccia, lo definiamo nel controller. È il regista che prende gli attori (model) e li fa recitare nelle scene del set (view). Quindi la maggior parte del codice che scriveremo sarà dentro al controller.

Riferimenti

Laravel: costruire tabelle, model, controller e creare dati con Faker

In questo articolo su Laravel popoleremo le tabelle di dati di test con il celebre plugin Faker: creeremo dapprima due tabelle database con una relazione 1 a molti con le migrazioni di Laravel, creeremo il model che le rappresenta, il Controller che le gestisce e le .

Creazione delle migrazioni

Creiamo le migrazioni due migrazioni per queste tabelle collegate da una relazione 1 a molti:

Diagramma  E/R per utilizzare Faker
Diagramma E/R per utilizzare Faker

Utilizziamo il comando make:model che ci consente di fare tutto in un colpo: creare la tabella, il model, il controller, e le factory per poter poplare con dati di test le tabelle.

Convenzione: creiamo il modello con il nome al singolare. Eloquent creerà la tabella con il nome al plurale, il Controller con il nome del model, le factories con il nome del model.

Laravel gestisce correttamente anche il fatto che Company è un plurale irregolare nella lingua inglese 🙂

$ php artisan make:model Company -mfscr
Model created successfully.
Factory created successfully.
Created Migration: 2021_11_24_182740_create_companies_table
Seeder created successfully.
Controller created successfully.

Questo comando crea:

  • un model (m) vuoto di nome Company;
  • una factory (f) vuota di nome CompanyFactory che ci servirà per definire i tipi di dati di test associare ai record della tabella;
  • una migrazione con la creazionedella tabella companies;
  • un seeder (s) per riempire di dati la tabella
  • un controller (c) di nome CompanyController
  • un resource (r) che popola il Controller con metodi standard CRUD (index(), create(), show(), edit(), update() e destroy())

Personalizzazione della migrazione

Dobbiamo definire come sarà la tabella:

class CreateCompaniesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('companies', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('website');
            $table->string('email')->unique();
            $table->float('latitude');
            $table->float('longitude');

            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('companies');
    }
}

Facciamo lo stesso per CompanyType:

$ php artisan make:model CompanyType -mfscr
Model created successfully.
Factory created successfully.
Created Migration: 2021_11_24_183431_create_company_types_table
Seeder created successfully.
Controller created successfully.

Sono stati creati i model, i controller con i 4 metodi standard, i factory.

Lancio della migrazione

Successivamente lanciamo le migrazioni:

$ php artisan migrate
Migrating: 2021_11_24_184611_create_company_types_table
Migrated:  2021_11_24_184611_create_company_types_table (136.22ms)
Migrating: 2021_11_24_184622_create_companies_table
Migrated:  2021_11_24_184622_create_companies_table (468.33ms)

Sono state create le tabelle e le relazioni:

faker_dbeaver_tables
Dbeaver mostra tabelle e relazionetra esse

Popolamento con dati di test con Faker

Per ultimo lanciamo il seeder che popola di dati la tabella Company Types. Lo facciamo rispettando la prirità che per rispettare la relazione di chiave esterna ci porta a popolare prima la tabella madre (company_types) e poi la figlia (companies).

Occorre dapprima scrivere cosa vogliamo che faccia il Seeder.

Per CompanyType definiamo innazitutto la factory (che definisce come devono essere popolati i campi)

<?php
// <APP>/database/factories/CompanyTypeFactory.php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class CompanyTypeFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'name' => $this->faker->name(),
        ];
    }
}

In questo caso abbiamo unsolo campo per cui utilizziamo un metodo “name” che genererà un nome proprio di persona. Non è questo il caso, ma Faker da la possibilità di generare anche indirizzi random, numeri di telefono, url, email latitudini e longitudini e molto altro ancora.

E poi scriviamo il seeder (definiamo quanti record devono essere creati, in questo caso 3):

<?php

// <APP>/database/seeders/CompanyTypeSeeder.php

namespace Database\Seeders;

use App\Models\CompanyType;
use Illuminate\Database\Seeder;

class CompanyTypeSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        CompanyType::factory()
            ->count(3)
            ->create();
    }
}

Infine per Company specifichiamo una chiave esterna fissa a 1

<?php

// <APP>/database/factories/CompanyFactory.php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class CompanyFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'name' => $this->faker->name(),
            'website' => $this->faker->url,
            'email' => $this->faker->email,
            'latitude' => $this->faker->latitude,
            'longitude' => $this->faker->longitude,
            'company_type_id' => 1,
        ];
    }
}

e popoliamo con 10 record:

<?php

// <APP>/database/seeders/CompanySeeder.php

namespace Database\Seeders;

use App\Models\Company;
use Illuminate\Database\Seeder;

class CompanySeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Company::factory()
            ->count(10)
            ->create();
    }
}

Per lanciare il comando si torna alla console e si scrive:

$ php artisan db:seed --class=CompanySeederSe

Se non specifichiamo la classe, lancerà tutti i seeder che abbiamo scritto.

Alla fine questo è il risultato:

Come Faker popola i record della tabella companies
Come Faker popola i record della tabella companies

È possibile anche specificare una localizzazione diversa dalla standard inglese, si va nel file di configurazione dell’applicazione <APP>/config/app.php e si modifica così la linea:

    'faker_locale' => 'it_IT',

Con la nuova localizzazione il risultato è questo

Laravel: Faker con localizzzione italiana
Laravel: Faker con localizzzione italiana

Risorse

CSRF come ti altero info riservate nel sito

Hacking technique csrf
L’attacco di tipo CSRF

Come funziona CSRF

Facciamo un po’ di personaggi e interpreti di questo articolo (CSRF = Cross Site Request Forgery = falsa richiesta tra siti):

  • la vittima: un utente che abitualmente visita e si autentica in
  • un sito che contiene un difetto per il quale si merita l’appellativo di sitovulnerabile.com;
  • un attaccante che è molto bravo e conduce
  • un sitomalevolo.com


Supponiamo che il servizio Web vulnerabile sia quello che consente agli utenti di modificare la loro email utilizzando solo un cookie per l’autenticazione e l’invio di un modulo.

Il sitomalevolo.com sembra innocuo ma ha al suo interno una pagina web con un programma JavaScript che invia di nascosto una richiesta al sitovulnerabile.com con una mail fasulla nel modulo e chiama semplicemente l’URL vulnerabile.

Facciamo un esempio. Il servizio del sitovulnerabile.com consente di modificare l’email dell’utente autenticato attraverso una form HTML: ebbene, l’Attaccante pubblica in sitomalevolo.com un form simile, che invia la stessa informazione al sitovulnerabile.com

<form action="https://sitovulnerabile.com/user/email" method="POST" onLoad="this.submit()">
    <input type="hidden" name="email" value="malicious-email@example.com">
    <input type="hidden" name="user_id" value="12345">
</form>

Requisito 1: l’attacco avrà successo se e solo se l’utente12345 esiste,

Poi l’Attaccante siede sulla riva del fiume ad attendere che passi la vittima, ovvero l’utente che viene in qualche modo indotto a visitare il sitomalevolo.com. Per esempio l’Attaccante adesca la vittima sottoponendola a SPAM e inviandogli una mail “Grossi premi alla Lotteria Web”; all’interno di questa mail mette un link che punta alla form sopra. Inoltre la form è auto inviante quindi la vittima il sito malevolo non lo vede neanche passare (può farlo se apre gli strumenti per sviluppatori Alt+Shift+I e controlla la scheda Network).

Requisito 2: la vittima dev’essersi precedentemente autenticata sul sito vulnerabile, per poter consegnare al sitovulnerabile.com, quando ritorna dopo l’hop nel sito malevolo, il cookie di sessione.

La richiesta quindi parte dal browser della vittima che attualmente era atterrato sul sito malevolo e invoca il sito vulnerabile chiamando la API di cambio mail e passandogli l’id_utente e il cookie di sessione.

Queste informazioni sono sufficienti a far sì che il sito vulnerabile aggiorni davvero la mail a insaputa dell’utente. Il danno può emergere anche dopo molto tempo, quando l’utente cercherà di entrare nuovamente nel sito vulnerabile (sarebbe meglio dire, a questo punto, sito vulnerato) ma questo gli risponde “User not known”.

Rimedio

Occorre aggiungere una terza informazione che certifica qual è l’origine del dato.

Laravel genera automaticamente un “token” CSRF per ogni sessione utente attiva gestita dall’applicazione. Questo token viene utilizzato per verificare che l’utente autenticato sia la persona che effettua effettivamente le richieste all’applicazione. Poiché questo token è archiviato nella sessione dell’utente e cambia ogni volta che la sessione viene rigenerata, un’applicazione dannosa non è in grado di accedervi.

È possibile accedere al token CSRF della sessione corrente tramite la sessione della richiesta o tramite l’helper csrf_token:

use Illuminate\Http\Request;

Route::get('/token', function (Request $request) {
    $token = $request->session()->token();

    $token = csrf_token();

    // ...
});

L’attaccante non può utilizzare questo metodo perché non gli ritornerebbe alcunché visto che non è autenticato (non ha e non avrà mai il cookie di sessione che invece è nel browser della vittima). E dovrebbe farlo per poter inserire nella form il valore del _csrf.

Ogni volta, infatti, che definisci un modulo HTML “POST”, “PUT”, “PATCH” o “DELETE” nella tua applicazione, dovresti includere un campo CSRF _token nascosto nel modulo in modo che il middleware di protezione CSRF possa convalidare la richiesta. Per comodità, puoi utilizzare la direttiva Blade @csrf per generare il campo di input del token nascosto:

<form method="POST" action="/user/email">
    @csrf

    <!-- Equivalente a... -->
    <input type="hidden" name="_token" value="{{ csrf_token() }}" />
</form>

Il middleware App\Http\Middleware\VerifyCsrfToken, incluso per impostazione predefinita nel gruppo middleware Web, verificherà automaticamente che il token nell’input della richiesta corrisponda al token archiviato nella sessione. Quando questi due token corrispondono, sappiamo che l’utente autenticato è quello che ha avviato la richiesta.

Riferimenti

Test Laravel

Laravel Test
Laravel test

In questo articolo porto un esempio di test con Laravel.

Laravel consente di eseguire due tipi di test: Unit per piccoli test su singole funzioni del controller o del model. Feature per testare intere funzionalità dell’applicativo dalla richesta HTTP alla produzione dell’output.

Laravel crea i test in due directory:

<APP>/tests/Feature
<APP>/test/Unit

Per creare un test (di defult si va sulle Features):

$ php artisan make:test UserTest [--unit]

Aggiungiamo --unit solo se vogliamo fare un test di unità (per un singolo metodo per esempio). Il nome del test possiamo sceglierlo come vogliamo, basta che ci ricordi cosa fa. Il nome finisce sempre per “Test”.

Faccio girare i test

$ php artisan test

   PASS  Tests\Unit\ExampleTest
  ✓ example

   PASS  Tests\Feature\ComplexTest
  ✓ example

   PASS  Tests\Feature\ExampleTest
  ✓ example

  Tests:  3 passed
  Time:   0.16s

Infatti quando creo i test la prima volta, lui aggiunge già un file tests/Feature/ExampleTest.php, io ne ho aggiunto un secondo che fa un test standard di risposta all’URL http://myapp.local/ (ho configurato il virtualhost Apache).

In questo esempio costruirò una classe per la gestione del tipo “numero complesso”, la classe Complex.

In una prima versione definisco il costruttore che crea l’oggetto Complex a partire dalla parte reale e immaginaria e poi scrivo qualche setter e qualche getter per consentire accesso e modifica alle variabili private $real e $imag:

<?php

namespace App;

use PHPUnit\Util\Exception;

class Complex
{
    private float $real;
    private float $imag;
    private float $modulus;
    private float $phase;

    public function __construct($x, $y)
    {
        $this->setReal($x);
        $this->setImag($y);
        $this->setModulus();
        $this->setPhase();

    }

    public function getReal(): float
    {
        return $this->real;
    }

    public function getImag(): float
    {
        return $this->imag;
    }

    public function getModulus(): float
    {
        return $this->modulus;
    }

    public function getPhase(): float
    {
        return $this->phase;
    }

    public function setReal($x)
    {
        $this->real = ($x == null) ? 0 : $x;
    }

    public function setImag($y)
    {
        $this->imag = ($y == null) ? 0 : $y;
    }

E scrivo alcuni test per mettere alla prova questi medodi: lo faccio nel file <APP>/test/Feature/ComplexTest.php;

class ComplexTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function test_example()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }

    public function test_setReal()
    {
        $r = 1;
        $z = new Complex(null, null);
        $z->setReal($r);
        $x = $z->getReal();

        $this->assertTrue($x == $r);

    }

    public function test_setImag()
    {
        $i = 1;
        $z = new Complex(null, null);
        $z->setImag($i);
        $y = $z->getImag();

        $this->assertTrue($y == $i);

    }

Lo spirito è il seguente: provo il metodo e confronto il risultato con quello che mi aspetto che faccia. Se non lo fa c’è un problema da risolvere. Per fare questo c’è il metodo assertTrue() che testa che una condizione sia vera. Ad esempio nel metodotest_setReal() viene creato un numero complesso nullo (con parte reale e immaginaria nulle) e poi viene impostata la parte reale a 1 con setReal(). Successivamente la leggo con getReal(); mi aspetto che il risultato sia 1. Per questo utilizzo il metodo assertTrue($x == $r).

Scrivo poi i metodi che impostano il modulo e la fase (già dentro al costruttore così le ho a disposizione fin dall’inizio) e con un metodo print() stampo il numero nella rappresenzazione cartesiana z = x + i y.

Per eseguire il test, lancio da riga di comando:

$ php artisan test
  PASS  Tests\Unit\ExampleTest
  ✓ example

   PASS  Tests\Feature\ComplexTest
  ✓ example
  ✓ set real
  ✓ set imag
  ✓ set modulus
  ✓ set phase
  ✓ print
  ✓ real print
  ✓ imag print
  ✓ modulus
  ✓ phase

   PASS  Tests\Feature\ExampleTest
  ✓ example

  Tests:  12 passed
  Time:   0.14s

$ 

Se invece qualcosa va storto si vede un output del genere:

$ php artisan test

   PASS  Tests\Unit\ExampleTest
  ✓ example

   FAIL  Tests\Feature\ComplexTest
  ✓ example
  ⨯ set real
  ⨯ set imag
  ✓ set modulus
  ✓ set phase
  ✓ print
  ✓ real print
  ✓ imag print
  ✓ modulus
  ✓ phase

   PASS  Tests\Feature\ExampleTest
  ✓ example

  ---

  • Tests\Feature\ComplexTest > set real
   PHPUnit\Util\Exception 

  Phase undefined

  at app/Complex.php:68
     64▕                 $res = M_PI_2;
     65▕             } else if ($this->imag < 0) {
     66▕                 $res = - M_PI_2;
     67▕             } else {
  ➜  68▕                 throw new Exception('Phase undefined');
     69▕             }
     70▕         }
     71▕ 
     72▕         return $this->phase = $res;

  1   app/Complex.php:19
      App\Complex::setPhase()

  2   tests/Feature/ComplexTest.php:27
      App\Complex::__construct()

e abbiamo tutti i dettagli per capire dove dobbiamo correggere.

Automazione dei test

Un aspetto ancora più bello di Laravel è che possiamo automatizzare i test quando vogliamo, ad esempio prima di un commit su Git.

Ma lo faccio in un altro articolo.

Riferimenti