Skip to content

5.14 Relaciones Muchos a Muchos en Laravel

5.14.1 Introducción

En los temas anteriores vimos:

  • Uno a Uno: un usuario tiene un teléfono.
  • Uno a Muchos: un usuario tiene muchos teléfonos.

En este tema trabajaremos con las relaciones Muchos a Muchos. Este tipo de relaciones es común en situaciones donde:

  • Un usuario puede tener varios roles (administrador, editor, lector).
  • Un curso puede tener varios estudiantes y un estudiante puede estar en varios cursos.
  • Un producto puede pertenecer a muchas categorías, y viceversa.

Diagrama general de una relación Muchos a Muchos

Diagrama de relación muchos a muchos

erDiagram
User ||--o{ role_user : has
Role ||--o{ role_user : has

User {
    int id
    string name
}

Role {
    int id
    string name
}

role_user {
    int id
    int user_id
    int role_id
    string added_by
}

Concepto clave

Para implementar esta relación, Laravel usa una tabla intermedia (pivot) que contiene las claves foráneas de ambas tablas.

Recordemos que con el planteaminto de la relación N:N lo que intentamos tener es tener una relación en la que un usuario puede tener varios roles y un rol puede pertenecer varios usuarios.


5.14.2 Creación de modelos migraciones y roles

5.14.2.1 Migraciones y seeders de User y Roles

Como ya tenemos el modelo User, este no lo vamos a crear pero sí que lo vamos a modificar para hacerlo más simple. Abrimo sla migración de users y la dejamos como sigue:

Migración de la tabla users

1
2
3
4
5
6
7
Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->unique();
    $table->string('password');
    $table->timestamps();
});

Suficiente con el nombre y el email, dejamos el password también para que no haya problemas de compatibilidad con el resto de la aplicación.

Ahora creamos el seeder de User para que se creen los usuarios automáticamente:

php artisan make:seeder UserSeeder

Por ejemplo con el siguiente contenido tenemos 4 usuarios:

Seeder UserSeeder.php

<?php
namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\User;

class UserSeeder extends Seeder
{
    /**
    * Run the database seeds.
    */
    public function run(): void
    {
        User::create([
            'name' => 'admin',
            'email' => 'admin@example.com',
            'password' => bcrypt('password'), // Use bcrypt for hashing
        ]);
        User::create([
            'name' => 'Jane Doe',
            'email' => 'jane@example.com',
            'password' => bcrypt('password'), // Use bcrypt for hashing
        ]);
        User::create([
            'name' => 'Donald Smith',
            'email' => 'donald@example.com',
            'password' => bcrypt('password'), // Use bcrypt for hashing
        ]);
        User::create([
            'name' => 'Mary Johnson',
            'email' => 'mary@example.com', 
            'password' => bcrypt('password'), // Use bcrypt for hashing
        ]);
    }
}

Ahora creamos el modelo Role y su migración:

php artisan make:model Role -m

Migración de roles:

Migración de la tabla roles

1
2
3
4
5
Schema::create('roles', function (Blueprint $table) {
    $table->id();
    $table->string('name')->unique();
    $table->timestamps();
});

El role es un nombre único, por lo que lo marcamos como unique.

Ahora creamos el seeder de Role para que se creen los roles automáticamente:

php artisan make:seeder RoleSeeder

Seeder RoleSeeder.php

<?php
namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\Role;

class RoleSeeder extends Seeder
{
    /**
    * Run the database seeds.
    */
    public function run(): void
    {
        Role::create(['name' => 'Administrador']);
        Role::create(['name' => 'Editor']);
        Role::create(['name' => 'Lector']);
    }
}

---

5.14.2.2 Tabla intermedia: role_user

En una relación muchos a muchos:

  • La tabla intermedia debe llamarse role_user (orden alfabético: role + user).
  • Debe contener los campos role_id y user_id.

Creamos la migración manualmente:

php artisan make:migration create_role_user_table

Migración tabla role_user

 /**
 * Run the migrations.
 */
public function up(): void
{
    Schema::create('role_user', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id');
        $table->foreignId('role_id');
        $table->string('added_by')->nullable();
        $table->timestamps();
        $table->unique(['user_id', 'role_id'], 'user_role_unique'); // Unique constraint for user_id and role_id
    });
}

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

foreignId

El método foreignId crea una columna de tipo unsignedBigInteger y establece una relación de clave foránea con la tabla correspondiente. En este caso, user_id y role_id son claves foráneas que hacen referencia a las tablas users y roles, respectivamente. Al haber mantenido el nombre de la tabla users y roles, Laravel automáticamente sabe a qué tabla referirse.

Campo adicional

En la tabla intermedia hemos establecido que las columnas user_id y role_id son claves foráneas que hacen referencia a las tablas users y roles. también son únicas (podrían formar la clave primaria) y no pueden ser nulas. Además, hemos añadido un campo added_by que puede ser útil para saber quién asignó el rol al usuario.

Ahora vamos con el seeder de RoleUserSeeder para que se creen los roles automáticamente. Pero a la hora de añadir un role a un user lo haríamos de la siguiente manera:

User::find(1)->roles()->attach(1, ['added_by' => 'sistema']);

Esto significa que el usuario con ID 1 (admin) tiene el rol con ID 1 (administrador) y que ha sido añadido por el sistema. Pero si nos fijamos, el modelo User está utilizando el método roles() que aún no hemos definido. Por tanto antes de crear el seeder de RoleUserSeeder vamos a definir las relaciones en los modelos User y Role.

Para ello tenemos que editar los dos modelos:

Modelo Role.php

<?php
class Role extends Model
{
    protected $guarded = [];

    public function users()
    {
        return $this->belongsToMany(User::class);
    }
}

Modelo User.php

<?php
class User extends Authenticatable
{
    use HasApiTokens, Notifiable;

    protected $guarded = [];

    public function roles()
    {
        return $this->belongsToMany(Role::class);
    }
}

Ahora sí, podemos crear el seeder de RoleUserSeeder para que se creen los roles automáticamente:

php artisan make:seeder RoleUserSeeder

Seeder RoleUserSeeder.php

<?php
namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\User;
use App\Models\Role;

class RoleUserSeeder extends Seeder
{
    /**
    * Run the database seeds.
    */
    public function run(): void
    {
        $admin = User::where('name', 'admin')->first();
        $jane = User::where('name', 'Jane Doe')->first();
        $donald = User::where('name', 'Donald Smith')->first();
        $mary = User::where('name', 'Mary Johnson')->first();

        $roleAdmin = Role::where('name', 'Administrador')->first();
        $roleEditor = Role::where('name', 'Editor')->first();
        $roleViewer = Role::where('name', 'Lector')->first();

        $admin->roles()->attach($roleAdmin->id, ['added_by' => 'sistema']);
        $jane->roles()->attach($roleEditor->id, ['added_by' => 'admin']);
        $jane->roles()->attach($roleViewer->id, ['added_by' => 'admin']);
        $donald->roles()->attach($roleViewer->id, ['added_by' => 'jane']);
        $mary->roles()->attach($roleViewer->id, ['added_by' => 'jane']);
        $mary->roles()->attach($roleEditor->id, ['added_by' => 'admin']);
    }
}

De esta manera, el usuario admin tiene el rol de administrador, Jane Doe tiene los roles de editor y lector, Donald Smith tiene el rol de lector y Mary Johnson tiene los roles de lector y editor.

Métodos para asignar roles

Método Descripción Ejemplo
attach() Asigna un rol a un usuario. $user->roles()->attach($roleId);
detach() Elimina un rol de un usuario. $user->roles()->detach($roleId);
sync() Sincroniza los roles de un usuario, eliminando los que no están en la lista y añadiendo los nuevos. $user->roles()->sync([$roleId1, $roleId2]);
toggle() Alterna la asignación de roles, añadiendo o eliminando según sea necesario. $user->roles()->toggle($roleId);
saveMany() Guarda múltiples instancias de un modelo relacionado. $user->roles()->saveMany([$role1, $role2]);

5.14.3 Relación en los Modelos

En User.php

Modelo User.php

1
2
3
4
public function roles()
{
    return $this->belongsToMany(Role::class);
}

5.14.4 Realizar la migración

Antes de realizar la migración recordar modificar el fichero DatabaseSeeder.php para que se ejecute el UserSeeder, RoleSeeder y RoleUserSeeder:

DatabaseSeeder.php

1
2
3
4
5
6
7
8
public function run(): void
{
    $this->call([
        UserSeeder::class,
        RoleSeeder::class,
        RoleUserSeeder::class,
    ]);
}

De esta manera, al ejecutar el migrate:refresh se ejecutarán los seeders y se crearán los usuarios y roles automáticamente.

Ahora que ya tenemos las migraciones, modelos y seeders listos, vamos a realizar la migración:

php artisan migrate:refresh --seed

Podemos comprobar en la base de datos que se han creado los usuarios y los roles.


5.14.5 Comprobar con Eloquent

$user = User::with('roles')->find(1);

foreach ($user->roles as $role) {
    echo $role->name;
}

Y al revés:

$role = Role::with('users')->find(1);

foreach ($role->users as $user) {
    echo $user->name;
}

5.14.6 Crear Vistas para Usuarios y Roles

Vamos a crear dos vistas web:

  1. Una ruta /roles que muestre todos los roles y sus usuarios asociados.
  2. Una ruta /users que muestre todos los usuarios y los roles que tiene cada uno.

Para ello, creamos dos controladores:

php artisan make:controller RoleController
php artisan make:controller UserController
Y los métodos index de ambos controladores:

RoleController.php
1
2
3
4
5
public function index()
{
    $roles = Role::with('users')->get();
    return view('roles.index', compact('roles'));
}
UserController.php
1
2
3
4
5
public function index()
{
    $users = User::with('roles')->get();
    return view('users.index', compact('users'));
}

Rutas en web.php

Route::get('/roles', [RoleController::class, 'index'])->name('roles.index');
Route::get('/users', [UserController::class, 'index'])->name('users.index');

Layout base (resources/views/_layouts/app.blade.php)

Creamos un layout base para las vistas:

<!DOCTYPE html>
<html>
<head>
    <title>@yield('title')</title>
</head>
<body>
    <h1>@yield('title')</h1>
    @yield('content')
</body>
</html>

Vista para Roles (resources/views/roles/index.blade.php)

@extends('_layouts.app')
@section('title', 'Listado de Roles')
@section('content')
    @foreach ($roles as $role)
        <h2>{{ $role->name }}</h2>
        <ul>
            @foreach ($role->users as $user)
                <li>{{ $user->name }}</li>
            @endforeach
        </ul>
    @endforeach
@endsection

Vista para Usuarios (resources/views/users/index.blade.php)

@extends('_layouts.app')
@section('title', 'Listado de Usuarios')
@section('content')
    @foreach ($users as $user)
        <h2>{{ $user->name }}</h2>
        <ul>
            @foreach ($user->roles as $role)
                <li>{{ $role->name }}</li>
            @endforeach
        </ul>
    @endforeach
@endsection

Controladores

Ambos controladores deben cargar la relación:

public function index()
{
    $roles = Role::with('users')->get();
    return view('roles.index', compact('roles'));
}
public function index()
{
    $users = User::with('roles')->get();
    return view('users.index', compact('users'));
}

5.14.7 Acceso a los campos de la tabla intermedia (pivote)

Recordemos que la tabla intermedia role_user tiene un campo adicional added_by. Para acceder a este campo, podemos usar el método withPivot() en la relación. Para eso tenemos que modificar los modelos User y Role para incluir este campo.

Modificación del Modelo User

1
2
3
4
public function roles()
{
    return $this->belongsToMany(Role::class)->withPivot('added_by');
}

Modificación del Modelo Role

1
2
3
4
public function users()
{
    return $this->belongsToMany(User::class)->withPivot('added_by');
}

Ahora en las vistas podemos acceder al campo added_by de la siguiente manera:

En la vista de Roles (resources/views/roles/index.blade.php)

Vista roles/index.blade.php

@extends('_layouts.app')
@section('title', 'Listado de Roles')
@section('content')
    @foreach ($roles as $role)
        <h2>{{ $role->name }}</h2>
        <ul>
            @foreach ($role->users as $user)
                <li>{{ $user->name }} (Añadido por: {{ $user->pivot->added_by }})</li>
            @endforeach
        </ul>
    @endforeach
@endsection

En la vista de Usuarios (resources/views/users/index.blade.php)

Vista users/index.blade.php

@extends('_layouts.app')    
@section('title', 'Listado de Usuarios')
@section('content')
    @foreach ($users as $user)
        <h2>{{ $user->name }}</h2>
        <ul>
            @foreach ($user->roles as $role)
                <li>{{ $role->name }} (Añadido por: {{ $role->pivot->added_by }})</li>
            @endforeach
        </ul>
    @endforeach
@endsection

5.14.8 Conclusiones

  • Las relaciones muchos a muchos requieren tres tablas: dos principales y una intermedia.
  • La tabla intermedia puede tener campos adicionales.
  • Laravel proporciona métodos como attach, detach y sync para gestionar estas relaciones.
  • El nombre de la tabla intermedia y de sus campos es importante para que Laravel la reconozca automáticamente.
  • Hemos visto cómo representarlo en vistas web.

Próximo tema: relaciones polimórficas, donde un mismo modelo puede relacionarse con varios otros tipos de modelos.