I am looking for a clean solution for querying and modifying my Eloquent model's dynamic properties, stored in another table.
My main model is User
. A User
may have multiple UserVariable
s. When loading the model through a UserRepository
, I may or may not eager-load the variables.
What I want to achieve is that the UserVariable
s can be modified in-memory, and be automatically saved when, and only when the User
is saved.
Here is my solution (stripped out the non-relevant parts), which works, but is nor elegant, nor scalable:
/**
* @property boolean $isCommentingEnabled
*/
class User extends \Illuminate\Database\Eloquent\Model
{
public function variables()
{
return $this->hasMany('UserVariable', 'var_userid', 'user_id');
}
public function getIsCommentingEnabledAttribute()
{
foreach ($this->variables as $variable) {
if ($variable->name == UserVariable::IS_COMMENTING_ENABLED) {
return (boolean) $variable->value;
}
}
return false;
}
public function setIsCommentingEnabledAttribute($enabled)
{
foreach ($this->variables as $variable) {
if ($variable->name == UserVariable::IS_COMMENTING_ENABLED) {
$variable->value = $enabled ? 1 : 0;
return;
}
}
$this->variables()->add(new UserVariable([
'name' => UserVariable::IS_COMMENTING_ENABLED,
'value' => $enabled ? 1 : 0,
]));
}
}
/**
* @property-read int $id Unique ID of the record.
* @property string $name Must be one of the constants in this class.
* @property int $userId
* @property string $value
*/
class UserVariable extends EloquentModel {}
class UserRepository
{
public function findById($id)
{
return User::with('variables')->find($id);
}
public function saveUser(User $user)
{
return $user->push();
}
}
This solution clearly doesn't scale. If the user had 5+ variables, the code would be abundant, even if I extracted the loops.
I suspect there must be a short and clean solution in Laravel to just pop out a User
's UserVariable
by name, or get a new one if it does not exist, modify its value and put it back to the model. When a User::push()
is called, it's auto-saved. Done.
I'm looking for something like
$user->variables()->where('name', UserVariable::IS_COMMENTING_ENABLED)->first()->value
= $enabled ? 1 : 0;
But the above does not work properly, because it uses the DB, not the model. Any help is appreciated.
Note: I'm working on a large legacy code base, so changing the DB structure is out of the question for now.
Unfortunately, there's no built-in way to do this. Fortunately, it's not too bad to just implement.
We have our model with its ->variables
relation.
class User extends \Illuminate\Database\Eloquent\Model {
public function variables() {
return $this->hasMany('UserVariable', 'var_userid', 'user_id');
}
We then need to build a method which tells us whether or not we've already loaded said relation, and one to load them if we haven't.
protected function hasLoadedVariables() {
$relations = $this->getRelations();
return array_key_exists('variables', $relations);
}
protected function loadVariablesIfNotLoaded() {
if(!$this->hasLoadedVariables()) {
$this->load('variables');
}
}
These methods allow us to build generic getters and setters for UserVariable
s.
The getter makes sure that the variables
relation is loaded, then returns the first value with a matching key.
protected function getVariable($key) {
$this->loadVariablesIfNotLoaded();
foreach($this->variables as $variable) {
if($variable->key === $key) {
return $variable->value;
}
}
return null;
}
The setter is a little more complicated because we want to update an existing UserVariable
model if one exists and create one if it doesn't, and in either case save the change in the ->variables
relation but not the DB. ( $this->variables->push($variable)
saves to the relation but not to the database).
protected function setVariable($key, $value) {
$this->loadVariablesIfNotLoaded();
foreach($this->variables as $k => $variable) {
if($variable->key === $key) {
$variable->value = $value;
return;
}
}
// We didn't find an existing UserVariable so we create one.
$variable = new UserVariable;
$variable->user_id = $this->id;
$variable->key = $key;
$variable->value = $value;
$this->variables->push($variable);
}
Laravel's ->push()
method does not save records which do not exist to the database by default, but we can override it to do so.
public function push(array $options = []) {
if($this->hasLoadedVariables()) {
foreach($this->variables as $variable) {
if(!$variable->exists) {
$this->variables()->save($variable);
} else if($variable->isDirty()) {
$variable->save();
}
}
}
parent::push();
}
Finally, with all that in place, we can add specific getters and setters for single attributes.
protected function setIsCommentingEnabledAttribute($enabled) {
$this->setVariable(UserVariable::IS_COMMENTING_ENABLED, $enabled);
}
protected function getIsCommentingEnabledAttribute($enabled) {
$this->getVariable(UserVariable::IS_COMMENTING_ENABLED);
}
}
To use this, just write code the way you normally would.
$user = User::find(123);
$user->is_commenting_enabled; // Calls the getter, no DB calls
$user->is_commenting_enabled = 1; // Calls the setter, no DB calls
$user->push(); // Saves the user and any variables which have been added or changed.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.