Models Eloquent

Com crear models Eloquent: convencions, propietats, timestamps, mass assignment i soft deletes.

Què és un model?#

Un model Eloquent és una classe PHP que representa una taula de la base de dades. Cada instància del model correspon a una fila de la taula. A través del model pots consultar, crear, actualitzar i eliminar registres sense escriure SQL, utilitzant una API orientada a objectes que és expressiva i segura.

Crear un model#

Per crear un model, utilitza la comanda Artisan make:model:

php artisan make:model Article

Això genera un fitxer a app/Models/Article.php amb l'estructura bàsica del model. Si necessites generar fitxers relacionats al mateix temps, pots combinar flags:

# Model + migració
php artisan make:model Article -m
 
# Model + migració + factory + seeder
php artisan make:model Article -mfs
 
# Model + migració + factory + seeder + controller + policy
php artisan make:model Article -mfsc --policy

El model generat té un aspecte com aquest:

namespace App\Models;
 
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
 
class Article extends Model
{
    use HasFactory;
}

El trait HasFactory permet que el model treballi amb factories per a la generació de dades de prova. A partir d'aquí, pots afegir propietats i mètodes per personalitzar el comportament del model.

Convencions de noms#

Eloquent segueix un conjunt de convencions que permeten que el model funcioni automàticament sense configuració addicional. La convenció més important és la relació entre el nom del model i el nom de la taula: Eloquent converteix el nom de la classe (singular, PascalCase) al nom de la taula (plural, snake_case). Per exemple, Article busca la taula articles, UserProfile busca user_profiles i Category busca categories.

La clau primària per defecte és id, un camp BIGINT UNSIGNED auto-increment. I Eloquent espera que la taula tingui dues columnes de timestamp: created_at i updated_at, que es gestionen automàticament cada cop que crees o actualitzes un registre.

Si la teva taula no segueix aquestes convencions, pots personalitzar-les al model:

class Article extends Model
{
    // Taula personalitzada
    protected $table = 'blog_posts';
 
    // Clau primària diferent
    protected $primaryKey = 'article_id';
 
    // Si la clau primària no és auto-increment
    public $incrementing = false;
 
    // Si la clau primària no és un integer
    protected $keyType = 'string';
 
    // Desactivar timestamps
    public $timestamps = false;
 
    // Personalitzar el format dels timestamps
    protected $dateFormat = 'U'; // Unix timestamp
 
    // Connexió de base de dades diferent
    protected $connection = 'mysql_secondary';
}

La majoria de projectes segueixen les convencions per defecte, cosa que evita haver de configurar res. Només personalitza aquestes propietats quan treballis amb taules que no segueixin l'estàndard de Laravel.

Operacions CRUD bàsiques#

Obtenir registres#

Hi ha diverses maneres de recuperar registres de la base de dades amb Eloquent:

// Obtenir tots els registres
$articles = Article::all();
 
// Trobar per ID
$article = Article::find(1);
 
// Trobar per ID o llançar 404
$article = Article::findOrFail(1);
 
// Trobar múltiples registres per ID
$articles = Article::find([1, 2, 3]);
 
// Primer registre que compleix la condició
$article = Article::where('slug', 'el-meu-article')->first();
 
// Primer registre o 404
$article = Article::where('slug', 'el-meu-article')->firstOrFail();

El mètode findOrFail() és especialment útil als controladors: si el registre no existeix, Laravel llança automàticament una excepció ModelNotFoundException que es converteix en una resposta 404. Això evita haver de comprovar manualment si el resultat és null.

Crear registres#

Per crear un nou registre, pots instanciar el model i cridar save(), o utilitzar el mètode estàtic create():

// Opció 1: instanciar i guardar
$article = new Article();
$article->title = 'El meu primer article';
$article->slug = 'el-meu-primer-article';
$article->body = 'Contingut de l\'article...';
$article->user_id = 1;
$article->save();
 
// Opció 2: create (assignació massiva)
$article = Article::create([
    'title' => 'El meu primer article',
    'slug' => 'el-meu-primer-article',
    'body' => 'Contingut de l\'article...',
    'user_id' => 1,
]);

El mètode create() crea el registre i el retorna en una sola operació. Per utilitzar-lo, els camps han d'estar definits a la propietat $fillable del model (veure la secció de Mass Assignment més avall).

Hi ha variants útils per a situacions concretes:

// Crear o retornar si ja existeix
$article = Article::firstOrCreate(
    ['slug' => 'el-meu-article'],           // Condició de cerca
    ['title' => 'El meu article', 'body' => '...']  // Valors per crear
);
 
// Obtenir el primer o instanciar sense guardar
$article = Article::firstOrNew(
    ['slug' => 'el-meu-article'],
    ['title' => 'El meu article']
);
 
// Crear o actualitzar
$article = Article::updateOrCreate(
    ['slug' => 'el-meu-article'],           // Condició de cerca
    ['title' => 'Títol actualitzat', 'body' => '...']  // Valors
);

Actualitzar registres#

Per actualitzar un registre, modifica els atributs i crida save(), o utilitza el mètode update():

// Opció 1: modificar i guardar
$article = Article::find(1);
$article->title = 'Títol actualitzat';
$article->save();
 
// Opció 2: update amb array
$article = Article::find(1);
$article->update(['title' => 'Títol actualitzat']);
 
// Actualitzar múltiples registres
Article::where('published', false)
    ->where('created_at', '<', now()->subYear())
    ->update(['archived' => true]);

Quan crides save(), Eloquent detecta automàticament si el model ja existeix a la base de dades (té una clau primària) i executa un UPDATE en lloc d'un INSERT.

Eliminar registres#

// Eliminar un registre
$article = Article::find(1);
$article->delete();
 
// Eliminar per ID directament
Article::destroy(1);
Article::destroy([1, 2, 3]);
 
// Eliminar amb condicions
Article::where('published', false)
    ->where('created_at', '<', now()->subYear())
    ->delete();

El mètode destroy() carrega el model abans d'eliminar-lo, cosa que fa que els events del model (deleting, deleted) es disparin correctament.

Mass Assignment#

L'assignació massiva és la capacitat de passar un array d'atributs directament als mètodes create() o update(). Per seguretat, Eloquent protegeix contra l'assignació massiva no desitjada: no podràs assignar camps que no estiguin explícitament permesos.

La propietat $fillable defineix quins camps es poden assignar massivament:

class Article extends Model
{
    protected $fillable = [
        'title',
        'slug',
        'body',
        'user_id',
        'published',
        'published_at',
    ];
}

Alternativament, pots utilitzar $guarded per definir quins camps NO es poden assignar massivament. Tots els altres camps queden permesos:

class Article extends Model
{
    // Tots els camps són assignables excepte 'id'
    protected $guarded = ['id'];
}

No utilitzis $guarded = [] (array buit) en producció. Això permet l'assignació massiva de qualsevol camp, cosa que pot ser un risc de seguretat si un atacant envia camps addicionals a la petició.

La protecció de mass assignment només afecta els mètodes que reben arrays (create(), update(), fill()). Les assignacions directes a propietats ($article->title = '...') sempre funcionen independentment de $fillable o $guarded.

Soft Deletes#

De vegades no vols eliminar registres permanentment de la base de dades, sinó marcar-los com a eliminats. Laravel proporciona el trait SoftDeletes que afegeix aquesta funcionalitat. En lloc d'eliminar la fila, Eloquent estableix una marca de temps a la columna deleted_at:

use Illuminate\Database\Eloquent\SoftDeletes;
 
class Article extends Model
{
    use SoftDeletes;
}

Per utilitzar soft deletes, la taula necessita la columna deleted_at. Afegeix-la a la migració:

Schema::table('articles', function (Blueprint $table) {
    $table->softDeletes(); // Afegeix la columna deleted_at
});

Un cop activat, les consultes normals exclouen automàticament els registres eliminats. Laravel afegeix un WHERE deleted_at IS NULL a cada consulta:

// Només retorna articles no eliminats
$articles = Article::all();
 
// Incloure els eliminats
$articles = Article::withTrashed()->get();
 
// Només els eliminats
$articles = Article::onlyTrashed()->get();
 
// Comprovar si un registre està eliminat
if ($article->trashed()) {
    // L'article està eliminat
}
 
// Restaurar un registre eliminat
$article->restore();
 
// Eliminar permanentment (sense possibilitat de restaurar)
$article->forceDelete();

Timestamps#

Per defecte, Eloquent gestiona automàticament les columnes created_at i updated_at. Quan crees un registre, created_at s'estableix a la data i hora actuals. Quan actualitzes un registre, updated_at es modifica automàticament.

Si vols actualitzar un registre sense modificar updated_at, utilitza el mètode withoutTimestamps():

Article::withoutTimestamps(fn () =>
    $article->update(['views' => $article->views + 1])
);

Per tocar el timestamp d'un registre sense canviar cap altre atribut, utilitza touch():

$article->touch(); // Actualitza updated_at a ara

Replicar models#

De vegades necessites crear una còpia d'un registre existent. El mètode replicate() crea una nova instància amb els mateixos atributs, però sense clau primària ni timestamps:

$original = Article::find(1);
 
$copy = $original->replicate();
$copy->title = 'Còpia de ' . $original->title;
$copy->slug = 'copia-de-' . $original->slug;
$copy->save();

Pots excloure atributs de la rèplica passant-los com a paràmetre:

$copy = $original->replicate(['published', 'published_at']);

Comparar models#

Eloquent proporciona mètodes per comparar instàncies de models:

// Comprovar si dos models són el mateix registre
if ($article->is($otherArticle)) {
    // Són el mateix registre (mateixa clau primària i taula)
}
 
// Comprovar si NO són el mateix
if ($article->isNot($otherArticle)) {
    // Són registres diferents
}
 
// Comprovar si un atribut ha canviat
if ($article->isDirty('title')) {
    // El títol ha estat modificat però encara no s'ha guardat
}
 
// Comprovar si res ha canviat
if ($article->isDirty()) {
    // Algun atribut ha canviat
}
 
// Comprovar si tot està net
if ($article->isClean()) {
    // Cap atribut ha canviat
}

Després de cridar save(), pots utilitzar wasChanged() per saber quins atributs s'han guardat:

$article->title = 'Nou títol';
$article->save();
 
$article->wasChanged();         // true
$article->wasChanged('title');  // true
$article->wasChanged('body');   // false