Le hasManyThrough est souvent perçu comme une relation un peu obscure notamment à cause de sa syntaxe. Pourtant, une fois compris, c’est une relation relativement simple à mettre et puissante, et parfaitement adaptée à certains cas d’usage bien précis.
Pour mieux comprendre son intérêt et son fonctionnement, nous allons imaginez le scénario suivant : une école (school) emploie des enseignants (teachers), et chaque enseignant a plusieurs élèves (students).
Notre objectif sera d'accéder directement aux élèves d’une école, sans passer manuellement par les enseignants.
Pour cela, nous allons mettre en place une relation hasManyThrough entre les écoles et les élèves, en nous appuyant sur trois tables : schools, teachers et students dont voici les migrations :
1Schema::create('schools', function (Blueprint $table) { 2 $table->id(); 3 $table->string('name'); 4 $table->timestamps(); 5}); 6 7Schema::create('teachers', function (Blueprint $table) { 8 $table->id(); 9 $table->string('name');10 $table->unsignedBigInteger('school_id');11 $table->timestamps();12});13 14Schema::create('students', function (Blueprint $table) {15 $table->id();16 $table->string('name');17 $table->unsignedBigInteger('teacher_id');18 $table->date('enrolled_at')->comment("Date d'inscription");19 $table->float('average_grade', 4, 2)->comment('Moyenne sur 20');20 $table->timestamps();21});
Les relations entre les différentes tables sont communes et s’organisent actuellement de la manière suivante :

Nous allons désormais créer nos trois modèles en respectant le jeu de relations ci-dessus. Commençons par ajouter notre modèle School, qui possédera de nombreux enseignants à l'aide d'une relation hasMany :
1namespace App\Models; 2 3use App\Models\Teacher; 4use Illuminate\Database\Eloquent\Model; 5use Illuminate\Database\Eloquent\Factories\HasFactory; 6 7class School extends Model 8{ 9 use HasFactory;10 11 public function teachers()12 {13 return $this->hasMany(Teacher::class);14 }15}
Chaque enseignant sera à la fois lié à une école et aura de nombreux étudiants, nous y retrouveront donc les relations belongsTo et hasMany :
1namespace App\Models; 2 3use Illuminate\Database\Eloquent\Model; 4use Illuminate\Database\Eloquent\Factories\HasFactory; 5 6class Teacher extends Model 7{ 8 use HasFactory; 9 10 public function school()11 {12 return $this->belongsTo(School::class);13 }14 15 public function students()16 {17 return $this->hasMany(Student::class);18 }19}
Pour finir, un étudiant sera lié à un enseignant via une relation belongsTo, et sera ainsi indirectement rattaché à une école par l’intermédiaire de ce dernier :
1namespace App\Models; 2 3use Illuminate\Database\Eloquent\Model; 4use Illuminate\Database\Eloquent\Factories\HasFactory; 5 6class Student extends Model 7{ 8 use HasFactory; 9 10 public function teacher()11 {12 return $this->belongsTo(Teacher::class);13 }14}
Tout est prêt pour manipuler les modèles et leurs relations !
À partir d’une école, nous pouvons désormais parcourir ses enseignants, puis pour chacun d’eux récupérer les élèves associés, voici à quoi ressemblerait le code :
1$school = School::with('teachers.students')->first(); 2 3echo 'School: ' . $school->name . PHP_EOL; 4 5foreach ($school->teachers as $teacher) { 6 7 echo 'Teacher: ' . $teacher->name . PHP_EOL; 8 9 foreach ($teacher->students as $student) {10 echo 'Student: ' . $student->name . PHP_EOL;11 }12}
Ce code fonctionne parfaitement, mais il est relativement verbeux et nécessite plusieurs requêtes pour remonter jusqu’aux élèves.
Maintenant, que faire si l’on souhaite récupérer directement les élèves d’une école, sans avoir à passer par les professeurs ?
C’est pour répondre à ce type de besoin qu’existe la relation hasManyThrough : elle permet d’accéder directement aux élèves liés à une école en passant "à travers" les professeurs, sans avoir à parcourir manuellement chaque relation.
Nous pouvons le schématiser de la manière suivante :

Commençons par ajouter une nouvelle relation students
directement dans notre modele School, cette relation déclare que nous passerons "à travers" le model Teacher afin d'y récupérer des étudiants :
1namespace App\Models; 2 3use App\Models\Teacher; 4use Illuminate\Database\Eloquent\Model; 5use Illuminate\Database\Eloquent\Factories\HasFactory; 6 7class School extends Model 8{ 9 use HasFactory;10 11 public function teachers()12 {13 return $this->hasMany(Teacher::class);14 }15 16 public function students() 17 {18 return $this->hasManyThrough(Student::class, Teacher::class);19 }20}
L’écriture de cette relation diffère légèrement des relations plus simples de Laravel et peut déstabiliser au premier abord. Une fois schématisée, la structure devient toutefois beaucoup plus claire :

Nous pouvons désormais modifier notre code afin de manipuler notre nouvelle relation students directement depuis une école :
1$school = School::first();2 3echo 'School: ' . $school->name . PHP_EOL;4 5foreach ($school->students as $student) {6 echo 'Student: ' . $student->name . PHP_EOL;7}
Ce code est plus court que le précédent et possède également moins de requêtes, Eloquent sera en mesure de récupérer tous les étudiants liés à une école sans avoir à manipuler de professeur !
Ce cas d’usage est relativement spécifique, ce qui explique probablement le peu d’occurrences de relations hasManyThrough dans nos projets, couplé à son écriture qui peut parfois être déstabilisante. Mais comme nous l’avons vu dans cet article, cette relation sera parfois très utile.
Quelques fonctionnalités supplémentaires
À l’instar des autres relations de Laravel, hasManyThrough possède quelques fonctionnalités supplémentaires qui pourront parfois nous simplifier la vie.
Tout d’abord, il sera possible de trier les étudiants directement depuis une école à l’aide des méthodes latestOfMany et oldestOfMany. Des méthodes similaires existent dans plusieurs autres relations, comme la morphMany que nous vous avions présentée précédemment :
1namespace App\Models; 2 3use App\Models\Teacher; 4use Illuminate\Database\Eloquent\Model; 5use Illuminate\Database\Eloquent\Factories\HasFactory; 6 7class School extends Model 8{ 9 use HasFactory;10 11 public function teachers()12 {13 return $this->hasMany(Teacher::class);14 }15 16 public function students()17 {18 return $this->hasManyThrough(Student::class, Teacher::class);19 }20 21 public function latestStudent() 22 {23 return $this->students()->one()->latestOfMany();24 }25 26 public function firstStudent()27 {28 return $this->students()->one()->oldestOfMany();29 }30}
Des nouvelles relations que vous utiliserez simplement :
1$school = School::first();2 3echo 'Latest Student: ' . $school->latestStudent->name . PHP_EOL;4 5echo 'First Student: ' . $school->firstStudent->name;
Attention cependant, ce tri s'effectue par défaut sur la colonne id de votre modèle et non, comme on pourrait l'imaginer, sur la valeur de la colonne created_at. Vous trouverez le code associé à ces deux méthodes dans la classe CanBeOneOfMany
du framework :
1// Illuminate\Database\Eloquent\Relations\Concerns\CanBeOneOfMany2public function latestOfMany($column = 'id', $relation = null)3{4 return $this->ofMany(Collection::wrap($column)->mapWithKeys(function ($column) {5 return [$column => 'MAX'];6 })->all(), 'MAX', $relation);7}
Une seconde fonctionnalité intéressante sera l’utilisation de la méthode ofMany
sur la relation, afin d’utiliser une fonction d’agrégation.
Comme par exemple récupérer l’étudiant avec la plus haute moyenne d’une école :
1 2namespace App\Models; 3 4use App\Models\Teacher; 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Factories\HasFactory; 7 8class School extends Model 9{10 use HasFactory;11 12 public function teachers()13 {14 return $this->hasMany(Teacher::class);15 }16 17 public function students()18 {19 return $this->hasManyThrough(Student::class, Teacher::class);20 }21 22 public function latestStudent()23 {24 return $this->students()->one()->latestOfMany();25 }26 27 public function firstStudent()28 {29 return $this->students()->one()->oldestOfMany();30 }31 32 public function studentsWithHighestAverageGrade() 33 {34 return $this->students()->one()->ofMany('average_grade', 'max');35 }36}
Nous avons approfondi l’utilisation de la méthode ofMany dans notre article consacré à la relation morphMany, que vous pourrez exploiter de manière similaire avec un hasManyThrough :
1$school = School::first();2 3echo 'Students with highest average grade: ' . $school->studentsWithHighestAverageGrade;
A lire
Autres articles de la même catégorie

Les Seeders
Créez rapidement des jeux de données grâce aux seeders !

Antoine Benevaut

Debugger les requêtes SQL dans Laravel
Comment faire pour debugger efficacement vos requêtes SQL dans Laravel ?

Mathieu De Gracia

Des dépendances stables pour une architecture de qualité
Appliquons le Principe des Dépendances Stable (SDP) dans une application Laravel

Mathieu De Gracia