innerbot's avatar

Eloquent Help: Generating Attribute Values Before Creating Record

On my User model, I have several fields that need to be populated before the record is created. I don't think this can be done using the User::$attributes property, because you can't specify a method to set a default value.

I could trigger them manually, but that's no fun, and shouldn't be necessary anyway.

The field(s) are complex in the sense that I'm using OpenSsl to generate a key-pair that gets saved to the database. The user's account uses these keys for various activities inside of the app.

So, I created a UserObserver class, and am attempting to populate those attributes using the creating hook.

However, when tinkering, I'm getting the following error when I call User::Create():

Indirect modification of overloaded property has no effect

Can anyone help me figure out the best way to make this happen?

Here is my user class:

<?php namespace App;

use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

use Carbon\Carbon;
use Hash;

class User extends Model implements AuthenticatableContract, CanResetPasswordContract {

    use Authenticatable, CanResetPassword;

    const STATUS_UNCONFIRMED = false;
    const STATUS_ACTIVE = true;

    const ROLE_ADMIN = 42;
    const ROLE_PUBLISHER = 1;

    /**
     * The database table used by the model.
     *
     * @var string
     */
    protected $table = 'users';

    /**
     * Default values for attributes
     * @var  array an array with attribute as key and default as value
     */
    protected $attributes = [
            'status' => self::STATUS_UNCONFIRMED,
            'role_id' => self::ROLE_PUBLISHER,
        ];

    /**
     * Protected attributes that CANNOT be mass assigned.
     *
     * @var array
     */
    protected $guarded = [ 'id', 'role_id', 'status', 'remember_token' ];

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = ['password', 'role_id', 'status', 'confirmation_code', 'private_key', 'public_key', 'remember_token'];

    /**
     * The attributes that are represented as dates
     *
     * @var array
     */
    protected $dates = ['last_login'];

    public function setPasswordAttribute( $pass ) {
    
        $this->attributes['password'] = Hash::make( $pass );
    
    }

}

And here is my UserObserver (registered in the EventServiceProvider boot method per Laravel Documentation):

<?php namespace App\Observers;

use Carbon\Carbon;

class UserObserver {

    private $user;


    public function creating( \App\User $model )
    {
        $this->user = &$model;

        $this->user->last_login = Carbon::now();

        $this->generateKeys();

        if( is_null($this->user->private_key) || is_null($this->user->public_key) )
            return false;

        $this->generateConfirmationCode();

        if( is_null($this->user->confirmation_code) )
            return false;

    }

    protected function generateKeys()
    {
        $pk_res = openssl_pkey_new( array(
            'private_key_bits' => 2048,
            'private_key_type' => OPENSSL_KEYTYPE_RSA
        ));

        openssl_pkey_export($pk_res, $this->user->private_key);

        $pubkey = openssl_pkey_get_details($pk_res);
        $this->user->public_key = $pubkey["key"];

        openssl_pkey_free($pk_res);
    }

    protected function generateConfirmationCode()
    {
        $this->user->confirmation_code = Hash::make( $this->user->email . time() );
    }

}

Still learning, here, so please let me know if I got it all wrong, haha. Thanks!

0 likes
6 replies
bobbybouwmann's avatar

Why not use an attribute mutator in your model? It will set the attribute before creating the model. I think the example for this is the password attribute.

setPasswordAttribute($value)
{
    $this->attributes['password'] = bcrypt($value);
}

Note: Laravel uses a convention for this. If your attributes is called public_key then the function name of the mutator needs to be setPublicKeyAttribute

3 likes
innerbot's avatar

Hi @blackbird, thanks for your reply.

I'm familiar with attribute mutators, the only reason I didn't think they were appropriate in this instance is that I didn't feel that client-code had any business handling the generation of the key-pairs or confirmation code. They only need to be touched during the initial creation of the User, and perhaps have a special event that is fire-able in the future to refresh them if necessary.

That's why I was looking for a way to automatically trigger the creation of these attribute values prior to the record being created in the database using the User::creating() event.

Does that sound like solid thinking?

I thought perhaps the reason I was getting the Indirect modification of overloaded property has no effect error was because I was not passing the User object to the UserObserver::creating() method without passing it in as a reference. I modified to the method signature to accept the $user var as a reference which changed the error I'm getting to read:

Parameter 1 to App\Observers\UserObserver::creating() expected to be a reference, value given

I'm not sure if I'm #doinItWrong or I've just made a small mistake that I'm not seeing.

Here is what my updated UserObserver class looks like:

<?php namespace App\Observers;

use Carbon\Carbon;
use App\User;

class UserObserver {

    private $user;


    public function creating( User &$model )
    {
        $this->user = &$model;

        $this->user->last_login = Carbon::now();

        $this->generateKeys();

        if( is_null($this->user->private_key) || is_null($this->user->public_key) )
            return false;

        $this->generateConfirmationCode();

        if( is_null($this->user->confirmation_code) )
            return false;

    }

    protected function generateKeys()
    {
        $pk_res = openssl_pkey_new( array(
            'private_key_bits' => 2048,
            'private_key_type' => OPENSSL_KEYTYPE_RSA
        ));

        openssl_pkey_export($pk_res, $this->user->private_key);

        $pubkey = openssl_pkey_get_details($pk_res);
        $this->user->public_key = $pubkey["key"];

        openssl_pkey_free($pk_res);
    }

    protected function generateConfirmationCode()
    {
        $this->user->confirmation_code = Hash::make( $this->user->email . time() );
    }

}
innerbot's avatar
innerbot
OP
Best Answer
Level 2

I finally managed to figure out a working solution.

First, I eliminated the UserObserver class in favor of using the User Model's boot method to set the creating event, and thus unregistered the UserObserver in the EventServiceProvider.

Then I moved the generate* methods I had created in the UserObserver class into my User model.

Finally, I changed the assignments of the attributes from $this->key to $this->attribute['key'] and voila! Everything now works as expected. Here is my updated User Model:

<?php namespace App;

use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

use Carbon\Carbon;
use Hash;

class User extends Model implements AuthenticatableContract, CanResetPasswordContract {

    use Authenticatable, CanResetPassword;

    const STATUS_UNCONFIRMED = false;
    const STATUS_ACTIVE = true;

    const ROLE_ADMIN = 42;
    const ROLE_PUBLISHER = 1;

    /**
     * The database table used by the model.
     *
     * @var string
     */
    protected $table = 'users';

    /**
     * Default values for attributes
     * @var  array an array with attribute as key and default as value
     */
    protected $attributes = [
            'status' => self::STATUS_UNCONFIRMED,
            'role_id' => self::ROLE_PUBLISHER,
        ];

    /**
     * Protected attributes that CANNOT be mass assigned.
     *
     * @var array
     */
    protected $guarded = [ 'id', 'role_id', 'status', 'remember_token' ];

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = ['password', 'role_id', 'status', 'confirmation_code', 'private_key', 'public_key', 'remember_token'];

    /**
     * The attributes that are represented as dates
     *
     * @var array
     */
    protected $dates = ['last_login'];

    /**
     * Boot function for using with User Events
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        static::creating(function ($model)
        {
            $model->generateKeys();
            $model->generateConfirmationCode();
        });
    }

    /**
     * Ensures that password is Hashed whenever assigned. Clear text passwords
     * are bad. Mmm'kay?
     * 
     * @var string $pass clear-text string password
     */
    public function setPasswordAttribute( $pass ) {
    
        $this->attributes['password'] = Hash::make( $pass );
    
    }

    /**
     * Generates a new 2048-bit RSA Key-Pair used for various User Activities
     * 
     * @return bool returns true if successful. false on failure.
     */
    protected function generateKeys()
    {
        $pk_res = openssl_pkey_new( array(
            'private_key_bits' => 2048,
            'private_key_type' => OPENSSL_KEYTYPE_RSA
        ));

        openssl_pkey_export($pk_res, $this->attributes['private_key']);

        $pubkey = openssl_pkey_get_details($pk_res);
        $this->attributes['public_key'] = $pubkey["key"];

        openssl_pkey_free($pk_res);

        if( is_null($this->attributes['private_key']) || is_null($this->attributes['public_key']) )
            return false;
        else
            return true;
    }

    /**
     * Generates the value for the User::confirmation_code field. Used to 
     * activate the user's account.
     * @return bool 
     */
    protected function generateConfirmationCode()
    {
        $this->attributes['confirmation_code'] = Hash::make( $this->email . time() );

        if( is_null($this->attributes['confirmation_code']) )
            return false; // failed to create confirmation_code
        else 
            return true;
    }

}
9 likes
Nospoon's avatar

I'm trying to do something similar, where I'm generating an api key and secret for the user upon creation, however for some reason the fields are not being persisted to the database. They're there when the object is created, but when I grab a fresh instance from db they're gone. Any ideas?

Nospoon's avatar

Ah, nevermind, I forgot to set the attributes array.

mreduar's avatar

Sorry if I'm reliving this very old thread, but I need some help for this situation.

@innYou said "perhaps have a special event that is fire-able in the future to refresh them if necessary"

Did you do that? Or can it only be used when creating the model?

Please or to participate in this conversation.