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

MansourM's avatar

is there a callback for when $with in a Model is loaded?

First i want to explain how i arrived here so you know what i'm trying to do, maybe i'm doing things wrong so any advice, or pointing me to a better way to do things will be appericiated.

How i got here:

I was researching on how to best implement my User types and roles and landed on this structure

users table:

| id | emaill | password | name | ...|

user_roles table:

| id | user_id| role_type_id| role_table_id| ...|

and then I have a separate table for each role, role_table_id is the id of the user in that table. for example here is admins table:

| id | user_id| is_super_admin| ...|

then I created my models which includes: User, Admin, Doctor, Farm, UserRole

User.php model:

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable, MyUserRoles;

    protected $table = 'users';
    protected $with = ['roles'];
    private $rolesPostProcessing;

    public function __construct($attributes = [])
    {
        parent::__construct($attributes);
        $this->rolesPostProcessing = [
            'already_processed' => false,
            'is_admin' => false,
            'is_doctor' => false,
            'is_farm' => false,
        ];
        //TODO: is there a callback to know when $with is loaded to run this there?
        // $this->processUserRoles();
    }

    public function adminDetails()
    {
        return $this->hasOne(Admin::class);
    }

    public function farmDetails()
    {
        return $this->hasOne(Farm::class);
    }

    public function doctorDetails()
    {
        return $this->hasOne(Doctor::class);
    }

    public function roles()
    {
        return $this->hasMany(UserRole::class);
    }
    //the rest of functions and vars
}

then i created a trait to do some postProcessing for me and add some helper functions

MyUserRoles.php trait:

trait MyUserRoles
{

    public function isAdmin()
    {
        $this->processUserRoles();
        return $this->rolesPostProcessing['is_admin'];
    }

    public function getAdminId()
    {
        $this->processUserRoles();
        return $this->rolesPostProcessing['admin_id'];
    }

    public function isDoctor()
    {
        $this->processUserRoles();
        return $this->rolesPostProcessing['is_doctor'];
    }

    public function getDoctorId()
    {
        $this->processUserRoles();
        return $this->rolesPostProcessing['doctor_id'];
    }

    public function isFarm()
    {
        $this->processUserRoles();
        return $this->rolesPostProcessing['is_farm'];
    }

    public function getFarmId()
    {
        $this->processUserRoles();
        return $this->rolesPostProcessing['farm_id'];
    }

    public function dumpProcessedRoles()
    {
        $this->processUserRoles();
        dump($this->rolesPostProcessing);
    }

    public function processUserRoles()
    {
        if ($this->rolesPostProcessing['already_processed'])
            return;

        foreach ($this->roles as $role) {
            switch ($role['role_type_id']) {
                case UserRole::Admin->value:
                    $this->rolesPostProcessing['is_admin'] = true;
                    $this->rolesPostProcessing['admin_id'] = $role['role_table_id'];
                    break;
                case UserRole::Doctor->value:
                    $this->rolesPostProcessing['is_doctor'] = true;
                    $this->rolesPostProcessing['doctor_id'] = $role['role_table_id'];
                    break;
                case UserRole::Farm->value:
                    $this->rolesPostProcessing['is_farm'] = true;
                    $this->rolesPostProcessing['farm_id'] = $role['role_table_id'];
                    break;
            }
            $this->rolesPostProcessing['role_table_id'] = $role['role_table_id'];
        }
        $this->rolesPostProcessing['already_processed'] = true;
    }

    public function loadAdminDetails()
    {
        $this->load("adminDetails")->get();
    }

    public function loadDoctorDetails()
    {
        $this->load("doctorDetails")->get();
    }

    public function loadFarmDetails()
    {
        $this->load("doctorDetails")->get();
    }
}

The Question:

the code above works fine but if you did not notice im calling $this->processUserRoles() at the start of evey function :D, so:

  1. is there any callback for $with=['roles'] used in User.php model so i can run $this->processUserRoles() there once? i tried but could not figure it out.
  2. [bonus:] is what i have done generally fine? I can't shake of the feeling that i'm doing it terribly wrong :)

P.S: about roles the thing is a user can be a farm, a admin or a doctor it can also be all at the same time so it's not a 1:1 scenario

0 likes
10 replies
vincent15000's avatar

Hmmm ... well ... not so easy to help you ... I need to understand some details.

First : why do you need all these helpers (isFarm(), getFarmId(), ...). If a user has one farm, you can easily get the farm's id by coding $user->farmDetails->id.

Second : to check if a user is a farm, you could also easily get this information by simply checking the role by coding $user->roles->contains('is_doctor').

MansourM's avatar

@vincent15000

about roles the thing is a user can be a farm, a admin or a doctor it can also be all at the same time so it's not a 1:1 scenario

Second: ( ill answer the second question first)

  • TBH i did not know about contains functions seems a very clean use, TY. but my iteration still seems okay since im extracting all info i need iterating through the array only once.

First:

  • I have a web dashboard and an mobile app, so i both have a api and web. i thought having those methods will be nice for checking access, authorize and specially on web when i want to decide which parts to render.
  • also im getting all this info without making extra db calls. my understanding is eloquent handles $with as a join and fetches data in 1 request. and since this are needed in every single request. maybe that matters in the long run.
1 like
vincent15000's avatar

@MansourM Ok ... The $with array is not a problem. I just wonder if all your helpers in the trait are really useful. Furthermore no matter which detail function you will use, all will return the user's id.

A roles table is essentially useful if you would have to add or delete some roles along the application's life.

I would do something easier where the roles are stored into the users table as a string.

users : id, name, password, roles // here the roles field has to be casted as an array

In the User model, you could have a public array with all roles.

public const $roles = [
	'admin',
	'doctor',
	'farm'
];

The roles could be stored in the database as a JSON field in the users table. It's often not recommended to use JSON fields, but just for the roles this should not impact the performances of the application.

Then you can easily retrieve each user's roles.

$user->roles;

And check if a user has a given role.

$user->roles->contains('doctor');
1 like
MansourM's avatar

@vincent15000 I understand what you mean, now that i think about it, laravel sanctum does the same thing in personal_access_token table.

1 like
vincent15000's avatar

@MansourM Yes and the spatie roles and permissions package also stores the roles into a JSON field.

1 like
Snapey's avatar

haven't you got an issue with collections of users? You won't be able to load a bunch of users with their roles.

and no, I'm not aware of any way to call the function automatically.

1 like
MansourM's avatar

@Snapey

I Tested getting many users with dump(User::all()->toArray())

array:10 [▼ // app\Http\Controllers\TestController.php:69
  0 => array:11 [▼
    "id" => 1
    "name" => "mname"
    "email" => "[email protected]"
    "email_verified_at" => "2023-08-14T07:12:20.000000Z"
    "mobile" => "09304112233"
    "is_active" => 1
    "avatar" => "avatar-6.jpg"
    "created_at" => "2023-08-14T07:12:20.000000Z"
    "updated_at" => "2023-08-14T07:12:22.000000Z"
    "roles" => array:3 [▼
      0 => array:6 [▶]
      1 => array:6 [▶]
      2 => array:6 [▶]
    ]
  ]
  1 => array:11 [▼
    "id" => 2
    "name" => "استاد برزین باغچه‌بان"
    "email" => "[email protected]"
    "email_verified_at" => "2023-08-14T07:12:22.000000Z"
    "mobile" => "09597099652"
    "is_active" => 1
    "avatar" => "avatar-4.jpg"
    "created_at" => "2023-08-14T07:12:22.000000Z"
    "updated_at" => "2023-08-14T07:12:22.000000Z"
    "roles" => array:1 [▼
      0 => array:6 [▼
        "id" => 4
        "user_id" => 2
        "role_type_id" => 1
        "role_table_id" => 2
        "created_at" => "2023-08-14T07:12:22.000000Z"
        "updated_at" => "2023-08-14T07:12:22.000000Z"
      ]
    ]
  ],
...
]

Everything seems fine to me.

1 like
Snapey's avatar

@MansourM but you are not actually getting the roles, just the pivot data

1 like
MansourM's avatar

@Snapey I get an array like this for each role a user has:

[▼
        "id" => 4
        "user_id" => 2
        "role_type_id" => 1
        "role_table_id" => 2
        "created_at" => "2023-08-14T07:12:22.000000Z"
        "updated_at" => "2023-08-14T07:12:22.000000Z"
      ]

since i have role_type_id i basically have the roles because the id matches

enum UserRole: int {
    case Admin = 0;
    case Doctor = 1;
    case Farm = 2;
}

this is kinda the whole point of MyUserRoles traint and the postProcessing I do.

actually one of things that i'm trying to better educate myslef on after this thread is to understand why i see in many places (like sanctum personal_access_token table, which i assume are using best practices) poeple are using json or string type instead of mapping these to an int or another value which should be much more effcient in indexing and stuff.

my understaning is if you can take a job and make is easier on DB (like saving int 0 instead of string "admin") then process this info on server code (or even better process on client, only when it make sense ofc) the end result will be more efficient for DB/Server.

1 like
vincent15000's avatar

@MansourM I have used both : int and string in different projects.

If you have fixed roles along the life of your application, it's much more practice to check if a user has the role admin instead of checking if he has the role 2, you remember best the string than the integer.

If you let the admin or the users create new roles in the application, then you will necessarily need to have a roles table, but how they are managed is then different.

1 like

Please or to participate in this conversation.