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

auflyer2's avatar

Extending User Model

I'm working on a project with the standard Laravel setup with a users table and User model. Where I'm getting stuck is a User can be an Athlete or a Coach (or neither). An Athlete can have many Coaches and vice versa.

At first I attempted to just extend the User model:

class Athlete extends User {



}

This is a nice clean solution, and follows what @Snapey recommends on this thread:

https://laracasts.com/discuss/channels/laravel/users-model-best-practices

From here I can set the relationship to the Coach

class Athlete extends User {

    /**
     * The coaches that belong to the athlete.
     */
    public function coaches()
    {
        return $this->belongsToMany('App\Coach');
    }

}

Here is where it starts breaking down, as the athlete_coach pivot table needs a coach_id and athlete_id to link the relationship. It seems like I'm just setting user_id for both fields. Something doesn't seem right, and gets even less clear when I try to add an 'athletes' and 'coaches' table.

I tried to do something like:

class Athlete extends User {

    protected $table = 'athletes';

}

But then this really mucks up the ability to call something like Athlete::name(); which is a method on the User. I'd prefer to have a way to call Athlete::all(), which seems to suggest I need a dedicated athletes table, but can't figure out how to work that in.

My workaround to this point has been to define a relationship from Athlete and Coach to the User. Like so:

class Athlete extends User {

   public function user(){
	return $this->belongsTo('App\User');
   }
 	
}

However this seems like it's going to get really messy as now I'm calling $athlete->user()->name(); when I really just want to be able to call $athlete->name(); This also has the side effect of duplicating a bunch of columns on the 3 tables (athletes, coaches, and users) and increasing the likelihood that they are going to get out of sync.

Like I said, it feels messy.

This seems like a fairly common use case so I'm wondering if anyone has a recommended technique.

Thanks!

0 likes
5 replies
bobbybouwmann's avatar
Level 88

You have two options here. Extending the user as you do now, or use something like a role in the users table. Then everything stays a User, but you have to query specifically for athletes or coaches.

Looking at your current example it would be better to continue with this. However, I would still recommend you to add a role or type column to your users table. The reason for this is that it makes it a lot easier to query them

class Athlete extends User
{
    protected static function booted()
    {
        static::addGlobalScope('athlete', function (Builder $builder) {
            $builder->where('type', 'athlete');
        });
    }
}

This way you can simply do Athele::all() and you will only get users back with that specific type.

For your relationships, you need to be a little bit creative. You need a many-to-many relation between athletes and coaches. Since you extend the User model you only have those ids, but you can name them whatever you want. So this should work

class Athlete extends User
{
    public function coaches()
    {
        return $this->belongsToMany(Coach::class, 'athlete_coach', 'athlete_id', 'coach_id');
    }
}

class Coach extends User
{
    public function athletes()
    {
        return $this->belongsToMany(Athlete::class, 'athlete_coach', 'coach_id', 'athlete_id');
    }
}

Your migration might look like this

Schema::create('athlete_coach', function (Blueprint $table) {
    $table->bigInteger('athlete_id')->unsigned();
    $table->bigInteger('coach_id')->unsigned();

    $table->foreign('athlete_id')->references('id')->on('users');
    $table->foreign('coach_id')->references('id')->on('users');
});

Let me know if that makes sense ;)

auflyer2's avatar

It does make sense. I really like the suggestion of a global scope on the Athlete model to filter the results from the users table. That was the piece I've been missing.

I'm using the Spatie Permissions package for Admin roles already, so I'm thinking I will just create a new role of Athlete and then add that check to the global scope definition.

Thanks!

auflyer2's avatar

I can now call Athlete::all() and it returns all Users that have a role of Athlete set.

One note, I'm on L6 for this project so when I copied Bobby's code from above it initially failed since booted() is introduced in L7. I switched to boot() and it works great:

https://laravel.com/docs/7.x/upgrade

Also of note, the belongsTo() must set the key to 'id' otherwise it will be looking for 'user_id'. This is only an issue because we're using the same table for both Models, yet they're related to each other...

Thanks again @bobbybouwmann for the assist:

use App\User;
use Illuminate\Database\Eloquent\Builder;
use Spatie\Permission\Models\Role;

class Athlete extends User
{
    protected $guarded = [];

    protected $table = 'users';

    protected static function boot() //must be boot() for <L7, booted() in L7
    {
        parent::boot(); //must be boot() for <L7, booted() in L7

        static::addGlobalScope('athlete', function (Builder $builder) {
            $builder->whereHas('user', function ($query) {
                $query->role('Athlete');
            });
        });
    }

    public function user()
    {
        return $this->belongsTo(User::class, 'id');
    }
}
1 like
bobbybouwmann's avatar

Awesome you got it working. I should have mentioned the new booted method though.

Please or to participate in this conversation.