Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

haakym's avatar

Dynamic Eloquent Relationship

I'm using eloquent and trying to set up what I would best describe as a dynamic relationship.

I have a Student model that can be on a Course: PhD, Master, Bachelor, Foundation or ESL and a student can have a report specific to each course, therefore I have the following eloquent models: PhdReport, MasterReport, BachelorReport, FoundationReport or EslReport.

One such relationship I require is to get the latest report for the student's current course. So if the student is on the bachelor course I want to grab the BachelorReport model where the date_to attribute is the most recent. This is my implementation on the Student model for the relationship:

public function latestReport()
{
    $courseReport = ucfirst(strtolower($this->course->name)) . 'Report'; 

    return $this->hasOne('\App\Models\\' . $courseReport)->latest('date_to');
}

This relationship works fine when I run the following code in artisan tinker:

// get a student
$student = App\Models\Student::first();

// student returned
=> <App\Models\Student #000000003dd8f51b00000000d869fe64> {
       course_id: 2, /* which is the master course */
       // other attributes ...
}

// get the latest report for the student's current course
$s->latestReport

// we get a MasterReport as expected
=> <App\Models\MasterReport #000000003dd8f51900000000d869fe64> {
    // attributes
}

However, when I run the following code:

$students = $this->student
    ->where('course', 2)
    ->where('is_active', '1')
    ->with('latestReport', /* other relationships */)
    ->get();

It always appears to run the code for PhdReport, this is shown when I check the Laravel debugbar

select * from `phd_reports` 
  where `phd_reports`.`student_id` in (/* list of ids */) 
  order by `date_to` desc

My current idea for a workaround is to conditionally apply a relation specific to the course/report:


$students = $this->student
    ->where('course', 2) /* for masters */
    ->where('is_active', '1');

if ($course == 'master') {
    $students = $students->with('latestMasterReport');
}

Any advice would be much appreciated. Thank you!

0 likes
13 replies
pmall's avatar

This is really not the way to go. The more "tricks" you'll use the less reliable your code will be.

It seems like you should have a Document model with a polymorphic relationship to a DocumentType model. This way your last report will always be a Document, regardless of the DocumentType it is linked to.

2 likes
haakym's avatar

Current solution is dynamically changing the relationship called, e.g.

$courseName = 'Master';

$students = Student::with('latest' . $courseName . 'Report')->get();

This does require a method for each report type in the student model which I'm guessing isn't the best solution but for now it works!

haakym's avatar

@pmall Thanks a lot for your reply.

By Document I assume you mean Report?

I'm not entirely sure how a polymorphic relationship solves the issue, but I'm most probably missing something!

Currently I have a abstract Report model which is the parent model of PhdReport, MasterReport, BachelorReport, FoundationReport and EslReport with each of the 5 aforementioned reports being different in their attributes. I can't see how I can work a ReportType model into this current set up as wouldn't this require me to scrap the 5 report models in favour of one Report model therefore assuming all the attributes for each report be the same?

I'll also add that a student could begin by being on the ESL course and therefore have an EslReport then later change to Master and have a MasterReport. What I was trying to do with the relation in the first post is get the latest report for the student's current course.

pmall's avatar

Have a Report model with a reports table. Student have relationship with the Report table. So no need for a "dynamic" relationship.

Then have a model/table for each report type, the Report model have a polymorphic has one relationship with those report types.

1 like
harryg's avatar

Polymorphism is the way to go. You would have a master Report model to which your student model would have the hasOne relationship with. This would have its own table. This Report model would then have its own polymorphic hasOne relationship to the sub-Report types; so the reports table would need reportable_id and reportable_type columns (doable with the $table->morphs('reportable'); migration helper) .

The relation in Report might look like:

public function report()
{
    return $this->morphTo();
}

So you can do $student->report->report to get the underlying report.

You can then put any fields pertinent to a specific report type in the sub-report table e.g. phd_reports.

If this is unclear it's worth watching this vid by Adam Wathan where he does a similar thing with coupons: http://adamwathan.me/2015/09/03/pushing-polymorphism-to-the-database/

2 likes
haakym's avatar

@pmall and @harryg - Many thanks for your replies and taking the time to explain the correct way to go about this.

I think I was failing to understand that each "subreport" (e.g. PhdReport) would have/be linked to a parent Report instance earlier.

However, I'm still failing to understand how I would implement the relation to retrieve the latest report for the student's current course using the approach you guys have suggested.

Just to make things clear, let's say we have a student whose current course is master and this student has two reports an EslReport and a MasterReport, how is the latestReport relation implemented to retrieve the report for his current course (the master report)???

harryg's avatar

@haakym So a student hasMany reports, not hasOne. So your relation on Student is as follows:

public function reports() 
{
    return $this->hasMany(Report::class);
}

You can then create a helper method to get the latest report:

public function latestReport()
{
    return $this->reports->latest('date_to')->first();
}

You would then access the latest student report like so:

$student->latestReport()->report; //gives the underlying report

You can also create methods on the parent report model to delegate down to the child reports to access their parameters without needing to specify the relation. E.g. a dynamic getter to get the sub-report's attribute if it doesn't exist on the parent report class.

1 like
haakym's avatar

@harryg

Thanks again for your response, however I don't think it addresses how to retrieve the latest report for the student's current course or even a specified course, unless I'm missing something.

Just to reiterate, I don't want the relationship to ONLY get the latest report, I want it to get the latest report which would be the report that is related to their current course. So for example, if the student's current course is master I want to retrieve the latest MasterReport linked to that student.

Thanks

harryg's avatar

@haakym Ah OK, well you can still do this with the arrangement I've advised, you'll just need to form the correct query.

For the Report model you should have a course relationship as well as the student relationship. The id of the related course would be stored in the course_id field of the reports table.

// Report.php

public function course()
{
    return $this->belongsTo(Course::class);
}

Then on your student model you can query the latest report for their course like so:

// Student.php
public function latestReport()
{
    return $this->reports()
                            ->where('course_id', $this->course_id)
                            ->latest('date_to')
                            ->first();
}

This will limit your query only to reports related to the Student's course. Of course you will need to set these relationships when creating the reports.

1 like
pmall's avatar

So I guess there is a table linking students and courses. Create a model representing this student/course pair (so the pair have an id). Then, reports are not linked to a student but to a student/course (the reports table contains a student/course id).

Then so for a student you just have to get its current student/course pair, then this student/course pair last report.

  • Student/course (find a suitable name for this) : id, student_id, course_id, created_at
  • reports : id, student/course_id, created_at
1 like
haakym's avatar

@harryg

Yep that's how I've got my course relationship set up. Okay that's pretty straight forward now I think about it, I think I'm still too wrapped up in my old DB structure to think outside the box! Ideally the latest report method would be return a relationship because I need to use it with the ->has and ->doesntHave eloquent methods (which I think only work with relationships?) so I'm guessing I can do something like:

public function latestReport()
{
    return $this->reports()
        ->where('course_id', $this->course_id)
        ->latest('date_to');
}

@pmall

That's quite a different approach to what I'm currently doing with the student - course relationship (which is currently one to many). I do need a way to keep track of the student's current course, so I'm assuming I'd keep the student's current course on the student model but then add an entry into the student/course table when creating a report to link it all up?

To both harryg and pmall - Thanks so much for your input on my question I really do appreciate your responses! I don't think I'll be able to implement all the suggestions you have made just yet due to time constraints on the project, but I definitely hope to come back to it very soon and implement your suggestions then hopefully post back here to benefit others and let you know what solution worked for me.

haakym's avatar

Many thanks @harryg and @pmall

I've implemented some of the suggestions you made and the structure although I think I could do a bit more refining the database is looking a lot better now! The adam wathan video was quite useful too.

Thanks again for your helpful advice.

izshreyansh's avatar

@PMALL -

The more "tricks" you'll use the less reliable your code will be.
Lol Best.

Please or to participate in this conversation.