简体   繁体   中英

Laravel Eloquent 5.1 update related model efficiently

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM