欢迎各位兄弟 发布技术文章

这里的技术是共享的

You are here

Eloquent Help: Generating Attribute Values Before Creating Record 默认值 有大用

 

  1. 可以使用
  2. public function setPublishedAtAttribute($date){
  3. // 未来日期的当前时间
  4. // $this->attributes['published_at'] = Carbon::createFromFormat('Y-m-d', $date);
  5. // 未来日期的0点
  6. $this->attributes['published_at'] = Carbon::parse($date);
  7. }


How to set a default attribute value for a Laravel / Eloquent model?

If I try declaring a property, like this:

public $quantity = 9;

...it doesn't work, because it is not considered an "attribute", but merely a property of the model class. Not only this, but also I am blocking access to the actually real and existent "quantity" attribute.

What should I do, then?

shareimprove this question
 

An update to this...

@j-bruni submitted a proposal and Laravel 4.0.x is now supporting using the following:

protected $attributes = array(
  'subject' => 'A Post'
);

Which will automatically set your attribute subject to A Post when you construct. You do not need to use the custom constructor he has mentioned in his answer.

However, if you do end up using the constructor like he has (which I needed to do in order to use Carbon::now()) be careful that $this->setRawAttributes() will override whatever you have set using the $attributes array above. For example:

protected $attributes = array(
  'subject' => 'A Post'
);

public function __construct(array $attributes = array())
{
    $this->setRawAttributes(array(
      'end_date' => Carbon::now()->addDays(10)
    ), true);
    parent::__construct($attributes);
}

// Values after calling `new ModelName`

$model->subject; // null
$model->end_date; // Carbon date object

// To fix, be sure to `array_merge` previous values
public function __construct(array $attributes = array())
{
    $this->setRawAttributes(array_merge($this->attributes, array(
      'end_date' => Carbon::now()->addDays(10)
    )), true);
    parent::__construct($attributes);
}

See the Github thread for more info: https://github.com/laravel/framework/issues/2265

shareimprove this answer
 
   
Thanks this helped a lot – jhamPac Nov 21 '14 at 19:25
   
Do you know how to make the value randomly generated rather than being the same every time the models gets instantiated? (this might be worth a separate question but I thought to comment first) – Petar VasilevOct 29 '15 at 12:57
1 
@PetarVasilev You can use the __construct override way above and call a function that generates your random value when assigning it. – cmfolio Nov 5 '15 at 17:42
   
I did that but when the user was created no data was saved in the database apart from my random value – Petar Vasilev Nov 11 '15 at 12:47
   
@PetarVasilev Sounds like it might be a mass assignment issue, but probably best to open a new topic for it. – cmfolio Nov 12 '15 at 18:01

This is what I'm doing now: 正确答案 

protected $defaults = array(
   'quantity' => 9,
);

public function __construct(array $attributes = array())
{
    $this->setRawAttributes($this->defaults, true);
    parent::__construct($attributes);
}

I will suggest this as a PR so we don't need to declare this constructor at every Model, and can easily apply by simply declaring the $defaults array in our models...


UPDATE:

As pointed by cmfolio, the actual ANSWER is quite simple:

Just override the $attributes property! Like this:

protected $attributes = array(
   'quantity' => 9,
);

The issue was dicussed here: https://github.com/laravel/framework/issues/2265

shareimprove this answer
 
   
J.Bruni, mind sharing the PR url? so we can keep track of it also :) – Tian Loon Jan 21 '14 at 10:30
   
@TianLoon, I've updated the answer. The original PR is here: github.com/laravel/framework/pull/2264 - I closed it myself after learning more about $attributes and acknowledging it was not necessary. See @cmfolio answer for details (he is using my proposed solution, because he needs to instantiate an object for one default). – J. Bruni Jan 21 '14 at 16:37
   
@J.Bruni hello, I was try by overrid the $attributes like your answer. when dd() the default attributes value show correctly. but the mutators attribute not work for this way. :/ i do this in laravel 5 – antoniputra Jun 4 '15 at 21:31

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.

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 thecreating 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!

Best Answer — Thread Owner's Choiceschool

innerbot
innerbot — 1 year ago

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;
    }

}
bobbybouwmann
 bobbybouwmann — 1 year ago

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

innerbot
innerbot — 1 year ago

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 theUserObserver::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() );
    }

}
innerbotcheck
innerbot — 1 year ago

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 thecreating 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;
    }

}
来自  https://laracasts.com/discuss/channels/eloquent/eloquent-help-generating-attribute-values-before-cre...

[Proposal] Provide a way to set default attribute values for an Eloquent model #2265

 Closed
jbruni opened this issue on 12 Sep 2013 · 10 comments

5 participants

@jbruni@bonzai@carlosipe@mhayes14@taylorotwell
@jbruni

As suggested here: http://stackoverflow.com/questions/18747500/how-to-set-a-default-attribute-value-for-a-laravel-eloquent-model/

After discussed here: http://irclogs.laravel.io/2013-09-11

I just want to make new instance of my Eloquent model to behave coherently with my database schema (which has default values set up).

I've created this PR: https://github.com/laravel/framework/pull/2264/files

I've closed the PR because I had not seen the "Laravel Contribution Guide" before I've opened it... anyway, since it is there, I am linking to it from here. Sorry, guys! I didn't know I had to open the Proposal first. I apologize.

Thank you.

 
@bonzai

:+1:

I would add the method getDefaults() so it would be possible to use defaults in schema migration.

 
@carlosipe

The typical use of a constructor is initializing values. Why does it deserves to be in the Eloquent Model? I mean, if you are needing this in a project you could extend Eloquent like

class MyBasicModel extends Eloquent{function __construct(){..}; function getDefaults()...etc};

and then:

   class AnyModel extends MyBasicModel

I think the way it works currently is just fine. Featuritis is a hard problem to solve and this PR doesn't bring us any needed functionality.

Sorry Jbruni, it's just my opinion. I hope it doesn't bother you..

 
@jbruni

I am actually extending Eloquent (in fact, the class name is Model) Ardent to create my own CustomModel class, base for all my application models, only because of this issue.

It is not as simple as stated above... the Model constructor accepts a parameter... we need to get it and pass to the parent::__construct... why all this hassle? And what if Ardent or Eloquent's Model constructor signature changes?

Not only this, but also: even inside the constructor (or "getDefaults"), the syntax needs to be $this->attributes['field_name'] = 'default';

All these considerations... only to have my default value there?

Doctrine also does not handle defaults, but everything is perfect if you simply assign a default value to a model property:

public $field = 'value';

Instead, using Eloquent's Model as it is now, one needs to do something like:

class MyModel extends Model {

    public __construct(array $attributes)
    {
        $this->attributes['field'] = 'value';
        parent::__construct($attributes);
    }

}

This doesn't seem quite right. But it is not equivalent to Doctrine's example yet! Keep reading.

We don't have tackled the core issue yet: by doing like above, $this->original becomes different than$this->attributes. Eloquent thinks the model is dirty. A call to getDirty will return true, even though we have merely set the database's default to the attribute!

This is why in the solution I am actually using in my extended Model base class, which can be seen both at Stack Overflow and the premature Pull request, I am using setRawAttributes... it is because, through this method we have syncOrignal called, and then the dirty issue is resolved.

Enough? Sounds too much to who just wanted to have a default value... luckily, Laravel's code is really good, well written, commented, understandable!

Finally, having tackled around this issue for a while, I propose a new and better solution.

In order to set default values (and this info should be included in the docs), just do:

class MyModel extends Eloquent {

    protected $attributes = array(
        'field'  => 'default',
        'field2' => 'default2',
    );

}

Voilà! BUT, in order for this to work, instead of declaring the same array again as protected $original, we just need to introduce a single new line into Eloquent's Model constructor:

   $this->syncOriginal();

This should go above the $this->fill($attributes); line.

Of course, instead of the syncOriginal() call, we could simply have:

    $this->original = $this->attributes;

With this simple, straight and innofensive line, it is possible to set defaults as described above. Just add a snippet about it to the docs, and others will not need to dive into all these Laravel's internals details mentioned above to find out how to do it. Not to mention not needing to extend, override constructor, and so on.

So, the Pull Request to solve the issue can be reduced to a single liner. What do you think?@taylorotwell? Makes sense?

Let's do a new comparison, considering this newly suggested PR was merged.

Doctrine:

public $field = 'value';

Eloquent:

protected $attributes = array('field' => 'value');

Slightly better than before, IMO.

 
@mhayes14

:+1: +1

 
@taylorotwell
The Laravel PHP Framework member

I don't even get the point of this? Your database should already insert the default values if you don't pass them, correct?

 
@mhayes14

So lets say you create a new model and omit the type field. In the database, this defaults to 'large' but your Eloquent model isn't aware of this because it hasn't been explicitly set. Most of the time it's nice to have complete data, for example if you want to do anything further with the newly created model. Choices are to either set the default values locally or to make another call to the database to get the 'complete' model, which slows things down.

 
@taylorotwell
The Laravel PHP Framework member

OK, so how is this different from just overriding the $attributes property on your model?

 
@mhayes14

Can this be done on the model itself? e.g.

class Table extends Eloquent {
    $attributes = [
        'type' => 'wooden',
        'legs' => 4
    ];
}
 
@jbruni
jbruni commented on 2 Oct 2013

@BlueHayes: New to Eloquent, I thought I could do:

class Table extends Eloquent {
    public $type = 'wooden';
    public $legs = 4;
}

After some iterations, and learning about it basically by looking at the Model class code, and also playing with it, I've understood the $attributes mechanism. It took me a little while.

So, yes, you can just override the $attributes property, but:

1 - Nobody at Stack Overflow could provide this information when asked; and not even right here (people is telling about extending / overriding constructor); right now you yourself are asking for confirmation! Anyway... a snippet of documentation would resolve this.

2 - By doing this (i.e., overriding $attributes), if you call isDirty in a new instance the result will be true, even though your model is not dirty. That's why my request became as simple as adding either a

$this->original = $this->attributes;

or

$this->syncOriginal();

line in the Model constructor, before the call to fill. It is innofensive and it properly and completely resolves the issue.

 
@taylorotwell
The Laravel PHP Framework member

Added syncOriginal before fill in parent constructor.

 
 



 

Default value for all Eloquent attributes

Hi,

I'd need to set a default value for any Eloquent model attribute. So, for example if I try to get $item->price, I'd need to set the value to "(empty)" to be echoed instead of an empty string. And I need to do that for every attribute in the model. I know how you can do that for specific attribute with getPriceAttribute method, but how to do it on all attributes?

Thanks for the help.

slovenianGooner
slovenianGooner
  • 2 years ago

You could make an ItemCreator method that specifically handles these things in some capacity.

https://github.com/ShawnMcCool/StructureExperimentation/blob/master/Domain/Posts/PostCreator.php

https://github.com/ShawnMcCool/StructureExperimentation/blob/master/Controllers/Posts/CreatePostController.php

Interesting concept. I import the data from an external source, maybe I could set the value when importing, but would prefer if the "(empty)" value would be only visible when outputting and not in database.

You could use a presenter class for your model, using the package by shawn ;)

https://github.com/ShawnMcCool/laravel-auto-presenter

Still, a good solution, but not general enough. Still have to define every attribute and it's output. Considering I got some amount of columns in the database it isn't exactly what I want.

Maybe you could try implementing model events restoring and saving (perhaps in your BaseModel) which would deal with conversion while retrieving and saving records.

http://laravel.com/docs/eloquent#model-events

This way you could adjust "filtering" of model attributes tied to your needs. Plain foreach loop on attributes swapping (empty) to emptystring and vice-versa would do the job I think.

In that case, derive your own class from Eloquent and override the getAttributeValue() method to return 'empty' if attribute == ""

来自 http://laravel.io/forum/02-13-2014-default-value-for-all-eloquent-attributes
普通分类: