May Sale! All accounts are 40% off this week.

signet_planet's avatar

Conflict with mutator on attribute when a Trait setter is being also used

Hi all, I've been away from programming for a long time expect a couple years ago when I started playing with Laravel to move an 18 year old php3 site to a more modern server and updated look. After all these years I am excited to be getting back to programming again as I've left paid employment (non-program related) and have committed to donating my time to develop software for a non-profit organization. I'm really loving Laravel 5.3 and it is sure exciting to see how far php has come.

Okay, on to my issue. I am making use of a Trait that is attached to all my models with the purpose being to encrypt and unencrypt data stored in the database and it worked well until I tried applying a mutator on one of those columns/attributes.

The issue I've run into is if I do a mutator on an attribute such as formatting a phone number, the encryption is done before that mutator is called so it's mutating the encrypted code which is of course not going to work.

In the trait my code to encrypt the attribute currently is:

public function setAttribute($key, $value)
{
    if (in_array($key, $this->encryptable)) {
        $value = Crypt::encrypt($value);
    }
    return parent::setAttribute($key, $value);
}

The mutator in the model is as:

public function setNumberAttribute($value) 
{
        $this->attributes['number'] = preg_replace("/[^[:digit:]]/u", '', $value);
}

So the trait's setAttribute() is being called first so it encrypts the $value, then the model's mutator setNumberAttribute($value) is being set.

I've tried various things to get the "set" mutator running before the trait but the deeper I dig the more that looks like I'm opening a can of worms. So at this point I see two directions I can go.

One is to move the encryption into mutators for each column to be encrypted in the database. That's a lot of repetition so not ideal but it would work.

The other direction is to replace my EncryptionTrait with a bigger MutatorTrait where I check for parameters set in the model of what attributes need what mutators and in what order and then it can call them dynamically. I think the only downside to that is then this can get to be a big Trait is being attached to every model.

I know Laravel sometimes has these hidden gems that just are not well known plus lots of you grasp PHP 5 and 7 OOP more than I do (I pretty much got out of programming just as php 5 was coming out) so I'm wondering if others have perhaps a simpler or better solution that I'm just not seeing (outside of the two options I've suggested of course).

0 likes
5 replies
bobbybouwmann's avatar
Level 88

I think there is a better solution for your problem. Laravel uses Model Events to handle these kind of cases. Everything that you need to know can be found in the documentation, but let me give you a small example

class SomeModel extends Model
{
    protected static function boot()
    {
        parent::boot();

        // This event will be called when you save or update the model
        static::saving(function ($model) {
            // Encrypt the fields here

            // An example would be creating a slug
            $model->slug = str_slug($model->title);
        });
    }
}

Documentation: https://laravel.com/docs/5.3/eloquent#events

Note: I showed you an example that you can use in your model. The documentation shows an example where you register the event in a ServiceProvider. It's up to you how to handle this ;)

1 like
signet_planet's avatar

Awesome, that looks a lot more flexible than mutators. However I'm going to see how it works using events with traits as the encrypt trait is considerably less overhead and coding than writing an event for each model. The events you showed me have lots of hooks of when they are run so I should be able to use it instead of mutators for items that are unique to that model. I'll do some testing and see how that goes.

When I did my initial reading of the docs to get into Laravel I read the events section https://laravel.com/docs/5.3/events but on my quick read it that seemed more like general housekeeping tasks that I assumed would run after everything else was done. So I never went back to looking at them. I admit that I completely missed seeing the events within the Eloquent section of the documentation where it shows all the hooks too allow you to access it at different stages. Pretty cool.

Thanks.

signet_planet's avatar

Okay, got a bunch of time trying different things. At first I thought your method breaks the SoftDeletes trait but then as I worked my way backwards I realized I forgot the parent::boot() call. Oops. But now it works great.

However digging around I see conflicting info on where to put the listener. Some say it is to be in the model's static boot() method as you have it. Others say to put it in EventServiceProvider boot method. Does it just boil down to preference as I see benefits to both places. Or is one method preferred to keep the system more compatible. I'm leaning towards putting it in the EventServiceProvider as then at a glance I can see all events happening.

Thanks for getting me pointed in the right direction. These event listeners are really great.

bobbybouwmann's avatar

It's all preference I guess. I like to keep them in my model. In your example you can also have an extra array property on your model that keeps a list of fields you want to encrypt and then use that in your boot method.

Please or to participate in this conversation.