In questo capitolo vedremo come creare la pagina del singolo prodotto sfruttando una caratteristica di Laravel che riguarda il binding tra modelli e route.
Binding implicito tra modelli e route
La caratteristica in questione è il binding implicito tra modelli e route. In pratica passando il modello come argomento del metodo del controller che gestisce la route, Laravel reperisce automaticamente i dati del modello dal database e li rende pronti per essere passati alla view Blade.
La sintassi di base è la seguente:
use App\Product;
Route::get('/prodotti/{product}', function (Product $product) {
return view('products.single', compact('product'));
});
Per impostazione predefinita Laravel utilizza l'ID del modello per reperire i dati dal database, ma volendo si può utilizzare una chiave diversa per rendere l'URL più SEO-friendly, come ad esempio lo slug del prodotto.
use App\Product;
Route::get('/prodotti/{product:slug}', function (Product $product) {
return view('products.single', compact('product'));
});
In questo caso un URL come /prodotti/test
userà lo slug test
per reperire il corrispondente prodotto dal database e renderlo disponibile alla view in Blade.
Andiamo quindi a definire il metodo del controller che gestirà questa particolare route.
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Product;
use Illuminate\Http\Request;
class ShopController extends Controller
{
public function single(Product $product)
{
$title = $product->title;
return view('products.single', compact('title', 'product');
}
}
Quindi andiamo a definire la route corrispondente in questo modo:
Route::get('/prodotti/{product:slug}', 'ShopController@single');
A questo punto possiamo definire la view Blade che avrà la seguente struttura.
@extends('layouts.site')
@section('content')
<article id="product">
<header class="mb-5">
<h1>{{ $product['title'] }}</h1>
</header>
<figure>
<img src="{{ url('assets/images') }}/{{ $product['image'] }}" alt="" class="img-fluid">
</figure>
<div class="row mt-4 mb-4">
<div class="col-md-2">
€ {{ $product['price'] }}
</div>
<div class="col-md-6">
<form class="form-inline" action="{{ route('add-to-cart') }}" method="post">
<div class="form-group">
<input type="number" name="quantity" value="1" min="1" step="1" class="form-control qty">
@csrf
<input type="hidden" name="id" value="{{ $product['id'] }}">
<input type="submit" class="btn btn-success" value="Aggiungi">
</div>
</form>
</div>
</div>
<p>{{ $product['description'] }}</p>
</article>
@endsection
Blade e direttiva @csrf
Nel form di aggiunta al carrello compare la direttiva di Blade @csrf
che merita una spiegazione a parte.
Questa direttiva infatti genera un campo di tipo hidden
che contiene un token univoco che serve ad identificare la richiesta POST per impedire attacchi da remoto al form di tipo CSRF (Cross-Site Request Forgery).
Laravel infatti richiede che tutte le richieste POST abbiano questo parametro, sia come campo da aggiungere al corpo della richiesta sia come header HTTP.
Se il parametro non viene impostato, Laravel impedisce che la richiesta venga processata e restituisce un errore HTTP.
Se si utilizza JavaScript con AJAX per inviare richieste POST, occorre impostare questo token come header HTTP per tutte le richieste di questo tipo.
Possiamo impostare un meta tag specifico per questo scopo:
<head>
<meta name="csrf-token" content="{{ csrf_token() }}">
</head>
Se si usa jQuery dobbiamo aggiungere l'header HTTP X-CSRF-TOKEN prima di ogni chiamata AJAX usando il metodo $.ajaxSetUp() globalmente.
$.ajaxSetup({
headers: {
"X-CSRF-TOKEN": $( 'meta[name="csrf-token"]' ).attr( "content" )
}
});
Se invece si utilizza la soluzione nativa delle Fetch API, occorre invece impostare l'header HTTP corrispondente nell'opzione headers
del metodo fetch()
.
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
}
};
In Vue.js il processo è invece suddiviso in due passaggi distinti. Nel primo aggiungiamo il token CSRF globalmente all'istanza di Vue.
<script>
window.Laravel = @php echo json_encode([
'csrfToken' => csrf_token(),
]); @endphp
</script>
Il secondo passaggio riguarda invece l'aggiunta effettiva del token a tutte le richieste AJAX effettuate tramite Vue. Si segue una procedura del tutto simile a quella vista in passato con AngularJS.
Vue.http.interceptors.push(function (request, next) {
request.headers['X-CSRF-TOKEN'] = Laravel.csrfToken;
next();
});
Salvataggio degli asset
La nostra view pone anche un'altra domanda al lettore attento: dove devono essere salvati gli asset in un applicativo in Laravel?
Per rispondere a questa domanda occorre fare una distinzione tra asset destinati al pubblico e asset privati destinati all'uso interno da parte dell'applicazione.
Supponiamo che la nostra applicazione preveda l'upload da parte dell'utente di un documento in cui viene sottoscritta la privacy policy del sito. Si tratta di dati sensibili che non possono essere esposti al pubblico.
Laravel offre la directory storage
come opzione per salvare questo tipo di dati. Si tratta di una directory interna fuori dalla document root del sito che, ricordiamolo, sarà sempre la directory public
.
Nel nostro caso invece stiamo inserendo le immagini dei prodotti che sono di fatto di pubblico dominio e che quindi è lecito salvare in una directory sotto la directory pubblica public
.
Un'altra feature da tenere presente è la direttiva route()
di Blade. Questa direttiva genera l'URL assoluto alla route il cui nome interno è quello che viene specificato come parametro di questa direttiva.
Questo ci ricorda l'importanza di assegnare un nome interno alle nostre route in modo da evitare di doverne inserire l'URL manualmente.
Conclusione
In questo capitolo abbiamo affrontato diversi argomenti correlati alla visualizzazione di un singolo prodotto. Nei prossimi capitoli vedremo cosa succede quando si vuole aggiungere un prodotto al carrello e più in generale la gestione del carrello stesso.