Laravel Eloquent: Guida Completa ai Model e alle Convenzioni (Lezione 18)

Esplora i Model Eloquent di Laravel, il cuore dell'interazione con il database, e scopri le convenzioni fondamentali che rendono lo sviluppo ORM intuitivo ed efficiente per i principianti.

Benvenuti alla diciottesima lezione del nostro corso 'Impara Laravel in 50 lezioni'! Oggi ci immergiamo in uno degli aspetti più potenti e amati di Laravel: i Model Eloquent e le convenzioni che li rendono così facili da usare. Se finora abbiamo parlato di routing, controller e viste, è giunto il momento di capire come Laravel gestisce l'interazione con il database in modo elegante e intuitivo.

L'interazione con il database è una parte cruciale di quasi ogni applicazione web. Tradizionalmente, questo significa scrivere query SQL complesse, gestire la connessione al database, e mappare manualmente i risultati in oggetti PHP. Laravel, con il suo ORM (Object-Relational Mapper) Eloquent, rivoluziona questo processo, permettendoti di interagire con il tuo database usando oggetti PHP in modo molto più naturale e leggibile. Preparati a scoprire come Eloquent semplifica enormemente il tuo lavoro, permettendoti di concentrarti sulla logica della tua applicazione piuttosto che sulla complessità del database.

Cos'è un Model Eloquent e Perché è Fondamentale?

Nel contesto del pattern MVC (Model-View-Controller) che Laravel adotta, i Model rappresentano la 'M'. Il loro ruolo principale è quello di interagire con il database e rappresentare la logica di business relativa ai dati. Ogni tabella del tuo database è tipicamente associata a un Model Eloquent.

Immagina di avere una tabella users nel tuo database. Invece di scrivere SELECT * FROM users WHERE id = 1; in SQL, con Eloquent potrai semplicemente scrivere User::find(1);. Questo non solo è più pulito e leggibile, ma ti fornisce un oggetto User con proprietà e metodi che riflettono le colonne della tua tabella e le operazioni che puoi eseguire su di esse. È come se ogni riga della tua tabella diventasse un'istanza di un oggetto PHP, con tutti i vantaggi della programmazione orientata agli oggetti.

Il Ruolo dell'ORM (Object-Relational Mapper)

Eloquent è un ORM. Un ORM è una tecnica di programmazione che mappa i dati da un sistema di tipi 'oggetto' a un sistema di tipi 'relazionale' (come un database relazionale) e viceversa. In pratica, trasforma le righe delle tabelle del tuo database in oggetti PHP e ti permette di manipolare questi oggetti come se fossero dati nativi di PHP, lasciando che l'ORM si occupi della traduzione in query SQL sottostanti.

Perché è così potente?

  1. Produttività: Meno codice da scrivere e meno tempo speso a scrivere SQL. Eloquent genera automaticamente le query per te.
  2. Leggibilità: Il codice PHP che usa Eloquent è molto più leggibile e intuitivo rispetto alle stringhe SQL.
  3. Manutenibilità: Modifiche allo schema del database possono richiedere meno modifiche al codice PHP, specialmente se si seguono le convenzioni.
  4. Sicurezza: Eloquent, come parte di Laravel, include protezioni integrate contro attacchi comuni come SQL Injection, gestendo automaticamente l'escaping dei dati.
  5. Coerenza: Fornisce un'API consistente per interagire con database diversi, astraendo le differenze tra MySQL, PostgreSQL, SQLite, ecc.

Le Convenzioni di Naming di Eloquent: La Magia Semplificata

Il vero potere di Eloquent, e ciò che lo rende così facile da imparare e usare, risiede nelle sue convenzioni di naming. Seguendo queste semplici regole, Eloquent può magicamente capire come connettere i tuoi Model alle tue tabelle del database senza che tu debba configurare quasi nulla. Questo riduce drasticamente la quantità di codice boilerplate che devi scrivere.

Ecco le convenzioni più importanti:

1. Nomi delle Tabelle (Plurale) e Nomi dei Model (Singolare)

  • Tabella: Deve essere al plurale e in snake_case (parole separate da underscore).
    • Esempio: users, products, blog_posts
  • Model: Deve essere al singolare e in PascalCase (prima lettera di ogni parola maiuscola, senza spazi).
    • Esempio: User, Product, BlogPost

Se hai una tabella chiamata users, Eloquent cercherà automaticamente un Model chiamato User. Se hai una tabella blog_posts, cercherà BlogPost. Questa è la convenzione più fondamentale e importante.

2. Chiave Primaria (Primary Key)

  • Nome: La chiave primaria di ogni tabella deve chiamarsi id.
  • Tipo: Deve essere un intero UNSIGNED BIGINT e auto-incrementante.

Eloquent si aspetta che la chiave primaria di ogni tabella sia una colonna chiamata id. Se la tua chiave primaria ha un nome diverso (es. user_id), dovrai specificarlo esplicitamente nel tuo Model usando la proprietà $primaryKey:

class MyModel extends Model
{
    protected $primaryKey = 'my_custom_id';
}

3. Timestamp

  • Nomi: Ogni tabella dovrebbe avere due colonne per i timestamp: created_at e updated_at.
  • Tipo: Entrambe devono essere di tipo TIMESTAMP o DATETIME.

Eloquent gestirà automaticamente l'aggiornamento di queste colonne. created_at verrà impostato alla creazione di una nuova riga, e updated_at verrà aggiornato ogni volta che una riga viene modificata. Questo è incredibilmente utile per tenere traccia delle modifiche ai tuoi dati. Se non vuoi che Eloquent gestisca i timestamp per un particolare Model, puoi disabilitarli:

class MyModel extends Model
{
    public $timestamps = false;
}

4. Chiavi Esterne (Foreign Keys)

Quando si stabiliscono relazioni tra tabelle (che vedremo in dettaglio nelle prossime lezioni), Eloquent segue anche qui delle convenzioni:

  • Per una relazione one-to-many (hasMany), la chiave esterna sulla tabella 'molti' dovrebbe essere il nome del Model singolare seguito da _id.
    • Esempio: Una tabella posts (Model Post) con una colonna user_id per relazionarsi con la tabella users (Model User).

Queste convenzioni sono il collante che tiene insieme Eloquent e le tue tabelle, rendendo la scrittura del codice più rapida e meno soggetta a errori. Più le segui, meno configurazione dovrai fare.

Creare il Tuo Primo Model Eloquent

Creare un Model è estremamente semplice grazie allo strumento da riga di comando di Laravel, Artisan. Useremo il comando make:model.

Apri il tuo terminale e naviga nella root del tuo progetto Laravel. Per creare un Model per la tabella products, esegui:

php artisan make:model Product

Questo comando creerà un nuovo file Product.php nella directory app/Models. Il contenuto di base sarà simile a questo:

<?php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
use Illuminate\\Database\\Eloquent\\Model;

class Product extends Model
{
    use HasFactory;
}

Complimenti! Hai appena creato il tuo primo Model Eloquent. Laravel ha già capito, grazie alla convenzione di naming, che questo Model Product si riferirà alla tabella products nel tuo database. Non c'è bisogno di specificare manualmente il nome della tabella, a meno che tu non voglia discostarti dalla convenzione. Se dovessi discostarti, potresti specificarlo così:

<?php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
use Illuminate\\Database\\Eloquent\\Model;

class Product extends Model
{
    use HasFactory;

    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'my_custom_products_table'; // Solo se il nome della tabella non è 'products'
}

Interagire con il Database tramite i Model

Ora che abbiamo un Model, vediamo come usarlo per eseguire le operazioni CRUD (Create, Read, Update, Delete) sul nostro database.

Assumiamo di avere una tabella products con colonne id, name, description, price, created_at, updated_at.

1. Recuperare Dati (Read)

Eloquent offre una vasta gamma di metodi per recuperare i dati. Tutti questi metodi sono statici, il che significa che li chiami direttamente sul nome del Model (Product::).

Recuperare tutti i record:

use App\\Models\\Product;

$products = Product::all(); // Restituisce una Collection di oggetti Product

foreach ($products as $product) {
    echo $product->name . " - " . $product->price . "\
";
}

Questo metodo all() recupera tutte le righe dalla tabella products e le converte in una Collection di oggetti Product.

Recuperare un singolo record per ID:

use App\\Models\\Product;

$product = Product::find(1); // Restituisce un singolo oggetto Product o null

if ($product) {
    echo "Nome prodotto: " . $product->name . "\
";
} else {
    echo "Prodotto non trovato.\
";
}

Il metodo find() è ottimo per recuperare un record specifico quando conosci il suo ID. Se il record non esiste, restituisce null.

Recuperare un singolo record o fallire:

use App\\Models\\Product;

// Questo lancerà un'eccezione ModelNotFoundException se il prodotto non esiste
try {
    $product = Product::findOrFail(1);
    echo "Nome prodotto: " . $product->name . "\
";
} catch (\\Illuminate\\Database\\Eloquent\\ModelNotFoundException $e) {
    echo "Errore: Prodotto non trovato.\
";
}

findOrFail() è utile quando ti aspetti che un record esista e vuoi che l'applicazione gestisca l'assenza come un errore (es. mostrando una pagina 404).

Recuperare record con condizioni (Query Builder):

Eloquent si integra perfettamente con il Query Builder di Laravel, permettendoti di costruire query complesse in modo fluente.

use App\\Models\\Product;

// Recupera prodotti con prezzo superiore a 100
$expensiveProducts = Product::where('price', '>', 100)->get();

foreach ($expensiveProducts as $product) {
    echo $product->name . " - " . $product->price . "\
";
}

// Recupera il primo prodotto con nome 'Laptop'
$laptop = Product::where('name', 'Laptop')->first();

if ($laptop) {
    echo "Trovato: " . $laptop->name . "\
";
}

// Recupera prodotti ordinati per nome e limitati a 5
$limitedProducts = Product::orderBy('name')->limit(5)->get();

I metodi come where(), orderBy(), limit() restituiscono istanze del Query Builder, permettendoti di concatenare più condizioni. get() esegue la query e restituisce una Collection, mentre first() restituisce il primo risultato come singolo Model o null.

2. Inserire Dati (Create)

Ci sono diversi modi per inserire nuovi dati. Il più comune è creare un'istanza del Model, assegnare le proprietà e poi chiamare il metodo save().

use App\\Models\\Product;

$newProduct = new Product();
$newProduct->name = 'Smartphone X';
$newProduct->description = 'Un potente smartphone di ultima generazione.';
$newProduct->price = 799.99;
$newProduct->save(); // Salva il nuovo prodotto nel database

echo "Prodotto creato con ID: " . $newProduct->id . "\
";

Il metodo save() gestirà automaticamente l'inserimento e imposterà created_at e updated_at.

Mass Assignment con create()

Per inserire più colonne contemporaneamente, puoi usare il metodo statico create(). Tuttavia, questo richiede una protezione contro il 'mass assignment'. Eloquent ti protegge da assegnazioni di massa non intenzionali. Devi specificare quali colonne sono 'fillable' (riempibili) nel tuo Model.

Nel tuo Product.php Model, aggiungi la proprietà $fillable:

<?php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
use Illuminate\\Database\\Eloquent\\Model;

class Product extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'description',
        'price',
    ];
}

Ora puoi usare create():

use App\\Models\\Product;

$product = Product::create([
    'name' => 'Smartwatch Y',
    'description' => 'Un orologio intelligente con molte funzionalità.',
    'price' => 249.99,
]);

echo "Prodotto creato con ID: " . $product->id . " (tramite create())\
";

In alternativa a $fillable, puoi usare $guarded per specificare le colonne che non possono essere assegnate in massa. Un array $guarded vuoto significa che tutte le colonne sono fillable (fatta eccezione per id). $guarded ha la precedenza su $fillable.

protected $guarded = []; // Tutte le colonne sono fillable tranne 'id'

3. Aggiornare Dati (Update)

Per aggiornare un record esistente, prima lo recuperi, modifichi le sue proprietà e poi chiami save().

use App\\Models\\Product;

$product = Product::find(1); // Supponiamo che il prodotto con ID 1 esista

if ($product) {
    $product->price = 849.99; // Modifica il prezzo
    $product->save(); // Salva le modifiche nel database. Aggiorna 'updated_at'
    echo "Prodotto ID " . $product->id . " aggiornato. Nuovo prezzo: " . $product->price . "\
";
} else {
    echo "Prodotto da aggiornare non trovato.\
";
}

Aggiornamento in massa con update()

Puoi anche aggiornare più record che soddisfano una certa condizione usando il metodo update() sul Query Builder. Questo non aggiorna i timestamp updated_at a meno che non sia specificato o che tu utilizzi un Model per l'update.

use App\\Models\\Product;

Product::where('price', '<', 50)->update(['price' => 50]);

echo "Tutti i prodotti con prezzo inferiore a 50 sono stati aggiornati a 50.\
";

4. Cancellare Dati (Delete)

Per cancellare un record, prima lo recuperi e poi chiami il metodo delete() sull'istanza del Model.

use App\\Models\\Product;

$product = Product::find(2); // Supponiamo che il prodotto con ID 2 esista

if ($product) {
    $product->delete(); // Cancella il prodotto dal database
    echo "Prodotto ID " . $product->id . " cancellato.\
";
} else {
    echo "Prodotto da cancellare non trovato.\
";
}

Cancellare record con condizioni:

Puoi anche cancellare più record che soddisfano una condizione senza recuperarli prima.

use App\\Models\\Product;

Product::where('price', '>', 1000)->delete();

echo "Tutti i prodotti con prezzo superiore a 1000 sono stati cancellati.\
";

Proprietà Importanti dei Model Eloquent

Oltre a $table, $primaryKey, $timestamps, $fillable e $guarded che abbiamo già visto, ci sono altre proprietà utili che puoi definire nei tuoi Model per personalizzarne il comportamento:

  • $connection: Se la tua applicazione usa più connessioni database, puoi specificare quale connessione deve usare il Model.
    protected $connection = 'mysql_secondary';
    
  • $casts: Questa proprietà ti permette di convertire automaticamente gli attributi del Model in tipi di dati PHP specifici quando vengono recuperati dal database o assegnati al Model. Questo è utilissimo per gestire date, JSON, booleani, ecc.
    protected $casts = [
        'email_verified_at' => 'datetime',
        'is_admin' => 'boolean',
        'options' => 'array',
        'price' => 'float',
    ];
    
    Con $casts, se la colonna is_admin nel database è un intero (0 o 1), verrà automaticamente convertita in un booleano true o false quando accedi a $user->is_admin.

Esempi Pratici: Gestione di un Sistema di Blog Semplice

Immaginiamo di voler gestire i post di un blog. Avremo una tabella posts e un Model Post.

Scenario: Creare, visualizzare, modificare ed eliminare i post del blog.

1. Creazione del Model e Migrazione

Prima di tutto, creiamo la migrazione per la tabella posts e il Model Post.

php artisan make:model Post -m

Il flag -m crea anche una migrazione. Modifica il file di migrazione appena creato (es. xxxx_create_posts_table.php) così:

<?php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content');
            $table->boolean('published')->default(false);
            $table->timestamps();
        });
    }

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

Esegui la migrazione:

php artisan migrate

Ora, nel tuo Model app/Models/Post.php, aggiungi $fillable e $casts:

<?php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
use Illuminate\\Database\\Eloquent\\Model;

class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'content',
        'published',
    ];

    protected $casts = [
        'published' => 'boolean',
    ];
}

2. Operazioni CRUD in un Controller (Esempio Concettuale)

Immagina di avere un PostController (che impareremo a creare e usare a breve).

<?php

namespace App\\Http\\Controllers;

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

class PostController extends Controller
{
    // Visualizza tutti i post
    public function index()
    {
        $posts = Post::all();
        // return view('posts.index', compact('posts')); // Esempio di come passeresti i dati alla vista
        return response()->json($posts); // Per semplicità, restituiamo JSON
    }

    // Mostra il form per creare un nuovo post (omesso per brevità)
    // public function create() { ... }

    // Salva un nuovo post nel database
    public function store(Request $request)
    {
        $validatedData = $request->validate([
            'title' => 'required|max:255',
            'content' => 'required',
            'published' => 'boolean',
        ]);

        $post = Post::create($validatedData);

        return response()->json(['message' => 'Post creato con successo!', 'post' => $post], 201);
    }

    // Visualizza un singolo post
    public function show(Post $post) // Route Model Binding: Laravel trova il post per ID automaticamente
    {
        return response()->json($post);
    }

    // Mostra il form per modificare un post (omesso per brevità)
    // public function edit(Post $post) { ... }

    // Aggiorna un post esistente
    public function update(Request $request, Post $post)
    {
        $validatedData = $request->validate([
            'title' => 'required|max:255',
            'content' => 'required',
            'published' => 'boolean',
        ]);

        $post->update($validatedData);

        return response()->json(['message' => 'Post aggiornato con successo!', 'post' => $post]);
    }

    // Elimina un post
    public function destroy(Post $post)
    {
        $post->delete();

        return response()->json(['message' => 'Post eliminato con successo!']);
    }
}

Questo esempio mostra come i Model Eloquent, combinati con le convenzioni e funzionalità come il Route Model Binding (che vedremo più avanti), semplificano enormemente la gestione dei dati in un'applicazione Laravel. Nota come non abbiamo scritto una singola riga di SQL, eppure stiamo interagendo pienamente con il database.

Errori Comuni e Come Risolverli

Quando si inizia con Eloquent, alcuni errori sono molto comuni. Ecco come affrontarli:

1. Mass Assignment Exception

  • Errore: Illuminate\\Database\\Eloquent\\MassAssignmentException o Add [field_name] to fillable property to allow mass assignment on [ModelName].
  • Causa: Stai cercando di usare create() o update() con un array di dati, ma non hai specificato le colonne come $fillable nel tuo Model o non hai usato $guarded correttamente.
  • Soluzione: Aggiungi le colonne che intendi assegnare in massa all'array $fillable nel tuo Model, oppure imposta $guarded = [] per permettere l'assegnazione di massa a tutte le colonne (fatta eccezione per id).

2. Table Not Found (Tabella non trovata)

  • Errore: Base table or view not found: 1146 Table 'database_name.model_name' doesn't exist
  • Causa: Eloquent sta cercando una tabella con un nome che non esiste nel tuo database. Spesso è dovuto a un errore di convenzione (es. Model Post ma tabella posts_table invece di posts).
  • Soluzione: Assicurati che il nome della tua tabella segua la convenzione (plurale, snake_case) rispetto al tuo Model (singolare, PascalCase). Se devi deviare dalla convenzione, specifica il nome della tabella con protected $table = 'nome_tabella_reale'; nel tuo Model.

3. Column Not Found (Colonna non trovata)

  • Errore: Unknown column 'column_name' in 'where clause' o simili.
  • Causa: Stai cercando di accedere o filtrare per una colonna che non esiste nella tua tabella database.
  • Soluzione: Controlla lo schema del tuo database e assicurati che la colonna esista e che il nome sia digitato correttamente (case-sensitive in alcuni DB, ma generalmente Laravel usa nomi snake_case che sono meno problematici).

4. Timestamp non gestiti correttamente

  • Causa: created_at e updated_at non vengono popolati automaticamente.
  • Soluzione: Assicurati che le colonne created_at e updated_at esistano nella tua tabella e siano di tipo TIMESTAMP o DATETIME. Se non vuoi che Eloquent le gestisca, imposta public $timestamps = false; nel tuo Model.

Prossimi Passi e Oltre i Fondamentali

Ora che hai una solida comprensione dei Model Eloquent e delle loro convenzioni, sei pronto per esplorare le funzionalità più avanzate che rendono Eloquent un ORM così potente:

  1. Relazioni Eloquent: La vera forza di Eloquent risiede nella sua capacità di gestire le relazioni tra i Model (one-to-one, one-to-many, many-to-many). Questo ti permetterà di interagire con i dati correlati in modo estremamente intuitivo, ad esempio $user->posts per ottenere tutti i post di un utente.
  2. Scopes: Permettono di definire query riutilizzabili e applicarle facilmente ai tuoi Model.
  3. Accessors e Mutators: Ti consentono di formattare gli attributi del Model quando vengono recuperati o impostati, ad esempio per trasformare una stringa in maiuscolo o per criptare una password.
  4. Eventi del Model: Puoi agganciarti a eventi specifici del ciclo di vita di un Model (es. creating, updating, deleting) per eseguire logica aggiuntiva.
  5. Factories: Strumenti per generare facilmente dati fittizi per i tuoi test e per popolare il tuo database durante lo sviluppo.

I Model Eloquent sono il pilastro di quasi ogni applicazione Laravel. Padroneggiare le convenzioni e le operazioni CRUD di base è il primo passo fondamentale per diventare uno sviluppatore Laravel efficiente e produttivo. Continua a praticare, sperimenta con diverse tabelle e scenari, e presto ti sentirai a tuo agio a manipolare i dati con la stessa facilità con cui scrivi codice PHP.

Nella prossima lezione, inizieremo a esplorare come definire e gestire le relazioni tra i tuoi Model, aprendo un mondo di nuove possibilità per la tua applicazione Laravel. Resta sintonizzato!