im puzzled by all the discussion and concerns of in-memory representation of roles?
everything is tore down and reloaded for each request cycle so why is there any need to wory about the effect of changing roles?
Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.
This question has a few parts. I'd greatly appreciate your help with any or all of them, since all the advice I was able to dig up was incomplete.
I'm looking for someone to point out my misunderstanding(s) or to offer better solutions than the ones I present.
Assume we have a model, User, that has a many-to-many relationship with another model, Roles. The User::roles relationship is set up using belongsToMany() etc.
I have two use cases and two desired implementations. So I have a total of 4 possible scenarios I would like to clarify.
The two use cases are:
The two desired implementations are:
I understand that sync(), attach(), and detach() all update the database, but it appears that they do NOT also update the in-memory data structure. This requires you to manually update the data structure to match the change you just made in the database. I have a solution (which I'll share below), but it seems totally unreasonable to me that we can update both.
I've also noticed that replacing $user->roles via assignment will break $user->save().
I'm getting ahead of myself. For thoroughness, let me explain each scenario...
Consider the following, which demonstrates how one might expect this to work...
assert( empty($user->roles) );
$user->roles()->attach($role); // or ->sync($role, false)
assert( count($user->roles) == 1 );
This will fail because, the in-memory collection has not updated. You must do either of the following...
$user->roles->push($role);
// OR
$user->roles->load('roles);
This seems unintuitive. I cannot imagine a case when you would want to update the database but NOT the model whose purpose is to model the data. This API uses that very model to desynchronize itself with its own datasource... using a method named "sync!" Madness!
Ok, I'm breathing. Breathing slowly...
Perhaps the app needs to do some sort of processing on the collection before we go saving to the database all willy nilly.
assert( empty($user->roles) );
$user->roles->push($role);
assert( count($user->roles) == 1 ); // right, this makes sense so far
// proprietary, industry disrupting, secret codes here
$user->roles-()->sync($user->roles);
assert( {{role is saved to database}} );
This works! But it's a little ugly. I would not consider this to be "eloquent."
Let's say we know what we want. We're strong, independent developers that don't need no existing data, or sanity checks, or stuffy business rules to hold us down.
assert( empty($user->roles) );
assert( count($newRoles) > 1 );
$user->roles = $newRoles;
$user->roles()->sync($user->roles); // jumping on the ugly train here
assert( count($user->roles) > 1 );
assert( {{roles are saved to database}} );
$user->save(); // SQL Exception!
Reassigning a relationship causes eloquent to see it as an real property on the model. So it tries to write to the "roles" column on the table, which doesn't exist.
And I really don't want to write a for loop every time I need to do this.
There is an alternative...
assert( empty($user->roles) );
assert( count($newRoles) > 1 );
$user->roles()->sync($newRoles);
assert( count($user->roles) > 1 ); // GAAH!!
assert( {{roles are saved to database}} );
$user->save();
Look familiar? Right. Moving along...
So we've grown up a little now, and we've realized that we really do need to pass $user through some sort of processing before saving it. But we're still deciding exactly which roles the user is in.
assert( empty($user->roles) );
assert( count($newRoles) > 1 );
$user->roles = $newRoles;
// top secret, world changing ... erm, look, it's just consolidated compliance certification audit business rule validation or something
// i just do what they tell me
// #synergy
$user->roles()->sync($newRoles);
assert( count($user->roles) > 1 ); // GAAH!!
assert( {{roles are saved to database}} );
$user->save();
Aaand we're back here again. We could give in and do the for loop thing. But we'd have to clear the collection first, and I found no way to do that. So we have to do this...
assert( empty($user->roles) );
assert( count($newRoles) > 1 );
removeMissingRoles($user, $newRoles);
addNewRoles($user, $newRoles);
// ...
$user->roles()->sync($newRoles);
assert( count($user->roles) > 1 ); // GAAH!!
assert( {{roles are saved to database}} );
$user->save();
function removeMissingRoles(User $user, Collection|Roles[] $roles) : void {
// remove from $user->roles where not in $roles
}
function addNewRoles(User $user, Collection|Roles[] $roles) : void {
// for each $roles
// push to $user->roles if not in $user->roles
}
Really? Really?! Do you know how bad that is? So bad, I won't even do it the honor of being written properly. Just no.
Please tell me I'm missing something grossly, embarrassingly obvious.
In my real-world use case, I ended up not (yet) needing to do some processing before saving. Here's what I'm doing for now...
trait MoreModelFeatures {
/**
* Add the given model to the given relationship.
* This will update both the database, and the in-memory collection.
* @param $relationship
* @param Model $model
*/
public function relate($relationship, Model $model) {
$this->$relationship->push($model);
$this->$relationship()->sync($model, false);
}
/**
* Add the given model to the given relationship.
* This will update both the database, and the in-memory collection.
* @param $relationship
* @param Model $model
*/
public function unrelate($relationship, Model $model) {
$this->$relationship()->detach($model);
$this->load($relationship);
}
}
It is used as follows...
$user->relate('roles', $role);
// happy hour
This allows me to update the model, the whole model, and nothing but the model ... and the database. If I want to replace all roles, I can't. Not yet. I'm going to write something like this later...
public function relateAll($relationship, $models){
$this->$relationship()->sync($d);
$this->load($relationship);
}
...but I don't like how this version does a full read from the database instead of just updating it in place.
Please or to participate in this conversation.