Relacions

Relacions Eloquent: hasOne, hasMany, belongsTo, many-to-many, polimòrfiques i taules pivot.

Com funcionen les relacions#

Les relacions d'Eloquent es defineixen com a mètodes al model. Quan crides el mètode com a propietat (sense parèntesis), Eloquent executa la consulta i retorna el resultat. Quan el crides com a mètode (amb parèntesis), retorna una instància del query builder de la relació, permetent-te encadenar condicions addicionals:

// Com a propietat: executa la consulta i retorna el resultat
$articles = $user->articles; // Collection d'articles
 
// Com a mètode: retorna el builder per afegir condicions
$published = $user->articles()->where('published', true)->get();

Aquesta distinció és fonamental: la propietat retorna dades, el mètode retorna un builder. Utilitzar la propietat és equivalent a fer $user->articles()->get().

One to One (Un a un)#

La relació un a un connecta un registre d'una taula amb exactament un registre d'una altra taula. Per exemple, un usuari té un perfil:

class User extends Model
{
    public function profile()
    {
        return $this->hasOne(Profile::class);
    }
}

Eloquent assumeix que la taula profiles té una columna user_id. Si el nom de la clau forana és diferent, pots especificar-lo:

return $this->hasOne(Profile::class, 'author_id');

La relació inversa utilitza belongsTo():

class Profile extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

Per accedir a la relació:

// Obtenir el perfil d'un usuari
$profile = $user->profile;
 
// Obtenir l'usuari d'un perfil
$user = $profile->user;
 
// Crear un perfil a través de la relació
$user->profile()->create([
    'bio' => 'Desenvolupador Laravel',
    'website' => 'https://exemple.com',
]);

Has One of Many#

De vegades vols obtenir un sol registre d'una relació "un a molts" basant-te en un criteri. Per exemple, l'última comanda d'un usuari:

class User extends Model
{
    public function latestOrder()
    {
        return $this->hasOne(Order::class)->latestOfMany();
    }
 
    public function oldestOrder()
    {
        return $this->hasOne(Order::class)->oldestOfMany();
    }
 
    public function largestOrder()
    {
        return $this->hasOne(Order::class)->ofMany('total', 'max');
    }
}

One to Many (Un a molts)#

La relació un a molts és la més comuna. Un usuari pot tenir molts articles, però cada article pertany a un sol usuari:

class User extends Model
{
    public function articles()
    {
        return $this->hasMany(Article::class);
    }
}
 
class Article extends Model
{
    public function author()
    {
        return $this->belongsTo(User::class, 'user_id');
    }
}

Quan el nom del mètode no coincideix amb el nom de la columna (aquí author en lloc de user), has d'especificar la clau forana. Si el mètode es digués user(), Eloquent inferiria automàticament user_id.

Per treballar amb la relació:

// Obtenir els articles d'un usuari
$articles = $user->articles;
 
// Amb condicions
$published = $user->articles()
    ->where('published', true)
    ->orderBy('published_at', 'desc')
    ->get();
 
// Comptar articles
$count = $user->articles()->count();
 
// Crear un article associat a l'usuari
$article = $user->articles()->create([
    'title' => 'Nou article',
    'slug' => 'nou-article',
    'body' => 'Contingut...',
]);
// El user_id s'assigna automàticament
 
// Associar un article existent
$article->author()->associate($user);
$article->save();
 
// Desassociar
$article->author()->dissociate();
$article->save();

Many to Many (Molts a molts)#

La relació molts a molts requereix una taula pivot intermèdia. Per exemple, un article pot tenir moltes etiquetes, i cada etiqueta pot pertànyer a molts articles:

La taula pivot#

Primer, crea la migració per a la taula pivot. Per convenció, el nom de la taula pivot és la combinació dels dos models en ordre alfabètic i singular:

// article_tag (no tag_article)
Schema::create('article_tag', function (Blueprint $table) {
    $table->id();
    $table->foreignId('article_id')->constrained()->cascadeOnDelete();
    $table->foreignId('tag_id')->constrained()->cascadeOnDelete();
    $table->timestamps();
});

Definir la relació#

class Article extends Model
{
    public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }
}
 
class Tag extends Model
{
    public function articles()
    {
        return $this->belongsToMany(Article::class);
    }
}

Si el nom de la taula pivot o les claus foranes no segueixen la convenció:

return $this->belongsToMany(Tag::class, 'article_tags', 'article_id', 'tag_id');

Gestionar les relacions pivot#

// Obtenir les etiquetes d'un article
$tags = $article->tags;
 
// Afegir etiquetes (sense eliminar les existents)
$article->tags()->attach([1, 2, 3]);
 
// Afegir amb dades extres a la taula pivot
$article->tags()->attach([
    1 => ['added_by' => auth()->id()],
    2 => ['added_by' => auth()->id()],
]);
 
// Eliminar etiquetes
$article->tags()->detach([1, 2]);
 
// Eliminar totes les etiquetes
$article->tags()->detach();
 
// Sincronitzar: estableix exactament aquestes etiquetes
$article->tags()->sync([1, 2, 3]);
// Si tenia [1, 2, 4], elimina 4 i afegeix 3
 
// Sincronitzar sense eliminar les que ja existeixen
$article->tags()->syncWithoutDetaching([1, 2, 3]);
 
// Alternar: afegeix si no existeix, elimina si existeix
$article->tags()->toggle([1, 2, 3]);

Accedir a dades de la taula pivot#

Si la taula pivot té columnes addicionals, pots accedir-hi a través de l'atribut pivot:

class Article extends Model
{
    public function tags()
    {
        return $this->belongsToMany(Tag::class)
            ->withPivot('added_by', 'notes')
            ->withTimestamps();
    }
}
 
foreach ($article->tags as $tag) {
    echo $tag->pivot->added_by;
    echo $tag->pivot->created_at;
}

Si vols un nom més descriptiu que pivot, utilitza as():

return $this->belongsToMany(Tag::class)
    ->as('tagging')
    ->withTimestamps();
 
// Ara accedeix amb:
$tag->tagging->created_at;

Filtrar per la taula pivot#

// Articles amb una etiqueta específica afegida aquest mes
$articles = Article::whereHas('tags', function ($query) {
    $query->wherePivot('created_at', '>=', now()->startOfMonth());
})->get();
 
// Etiquetes ordenades per la data d'assignació
$tags = $article->tags()->orderByPivot('created_at', 'desc')->get();

Has Many Through#

La relació "has many through" proporciona una drecera per accedir a registres distants a través d'una relació intermèdia. Per exemple, un país té molts articles a través dels seus usuaris:

class Country extends Model
{
    public function articles()
    {
        return $this->hasManyThrough(Article::class, User::class);
    }
}

Eloquent construeix automàticament la consulta: el país té usuaris (users.country_id), i cada usuari té articles (articles.user_id). Pots especificar les claus si les convencions no s'apliquen:

return $this->hasManyThrough(
    Article::class,  // Model final
    User::class,     // Model intermedi
    'country_id',    // Clau forana del model intermedi
    'user_id',       // Clau forana del model final
    'id',            // Clau local del model actual
    'id'             // Clau local del model intermedi
);

Relacions polimòrfiques#

Les relacions polimòrfiques permeten que un model pertanyi a més d'un tipus de model amb una sola relació. L'exemple clàssic són els comentaris: tant els articles com els vídeos poden tenir comentaris.

Polimòrfica un a molts#

La taula comments necessita dues columnes especials: commentable_id (l'ID del model pare) i commentable_type (la classe del model pare):

Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->text('body');
    $table->morphs('commentable'); // Crea commentable_id i commentable_type
    $table->foreignId('user_id')->constrained();
    $table->timestamps();
});

El mètode morphs() crea les dues columnes i els índexs necessaris. A continuació, defineix les relacions als models:

class Comment extends Model
{
    public function commentable()
    {
        return $this->morphTo();
    }
 
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}
 
class Article extends Model
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}
 
class Video extends Model
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

Per treballar amb les relacions:

// Obtenir els comentaris d'un article
$comments = $article->comments;
 
// Obtenir el model pare d'un comentari (Article o Video)
$commentable = $comment->commentable;
 
// Crear un comentari per un article
$article->comments()->create([
    'body' => 'Bon article!',
    'user_id' => auth()->id(),
]);

Polimòrfica molts a molts#

Per a etiquetes que poden pertànyer a articles, vídeos o qualsevol altre model:

Schema::create('taggables', function (Blueprint $table) {
    $table->foreignId('tag_id')->constrained();
    $table->morphs('taggable');
});
 
class Tag extends Model
{
    public function articles()
    {
        return $this->morphedByMany(Article::class, 'taggable');
    }
 
    public function videos()
    {
        return $this->morphedByMany(Video::class, 'taggable');
    }
}
 
class Article extends Model
{
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

Consultar relacions#

Eloquent proporciona mètodes per filtrar models basant-se en l'existència o les propietats de les seves relacions:

// Articles que tenen almenys un comentari
$articles = Article::has('comments')->get();
 
// Articles amb més de 5 comentaris
$articles = Article::has('comments', '>=', 5)->get();
 
// Articles amb comentaris que compleixin una condició
$articles = Article::whereHas('comments', function ($query) {
    $query->where('approved', true);
})->get();
 
// Articles sense comentaris
$articles = Article::doesntHave('comments')->get();
 
// Articles sense comentaris aprovats
$articles = Article::whereDoesntHave('comments', function ($query) {
    $query->where('approved', true);
})->get();

Pots combinar withCount() amb has() per filtrar i comptar al mateix temps:

$articles = Article::withCount('comments')
    ->having('comments_count', '>', 10)
    ->get();

Relacions per defecte#

Quan una relació belongsTo retorna null (per exemple, un article sense autor), pot causar errors en accedir a propietats. El mètode withDefault() retorna un model buit en lloc de null:

class Article extends Model
{
    public function author()
    {
        return $this->belongsTo(User::class, 'user_id')
            ->withDefault();
    }
}
 
// Ara $article->author sempre retorna un User (pot ser buit)
echo $article->author->name; // '' en lloc de Error: property of null
 
// Amb valors per defecte
return $this->belongsTo(User::class)->withDefault([
    'name' => 'Autor desconegut',
]);