22

This may seem like a trivial question, however all of the obvious solutions that I can think of have their own flaws.

What we want is to be able to set any default ActiveRecord attribute value for new records only, in a way that makes it readable before and during validation and does not interfere with derived classes used for search.

The default values need to be set and ready as soon as we instantiate the class, so that (new MyModel)->attr returns the default attr value.

Here are some of the possibilities and the problems they have:

  • A) In MyModel override the init() method and assign default value when isNewRecord is true like so:

    public function init() {
        if ($this->isNewRecord) {
            $this->attr = 'defaultValue';
        }
        parent::init();
    }
    

    Problem: Search. Unless we explicitly unset our default attribute in MySearchModel (very error-prone because it is too easy to forget), this will also set the value before calling search() in the derived MySearchModel class and interfere with searching (the attr attribute will already be set so search will be returning incorrect results). In Yii1.1 this was resolved by calling unsetAttributes() before calling search(), however no such method exists in Yii2.

  • B) In MyModel override the beforeSave() method like so:

    public function beforeSave($insert) {
        if ($insert) {
            $this->attr = 'defaultValue';
        }
        return parent::beforeSave();
    }
    

    Problem: Attribute is not set in unsaved records. (new MyModel)->attr is null. Worse yet, even other validation rules that rely on this value will not be able to access it, because beforeSave() is called after validation.

  • C) To ensure the value is available during validation we can instead override the beforeValidate() method and set the default values there like so:

    public function beforeValidate() {
        if ($this->isNewRecord) {
            $this->attr = 'defaultValue';
        }
        return parent::beforeValidate();
    }
    

    Problem: Attribute is still not set in unsaved (unvalidated) records. We need to at least call $model->validate() if we want to get the default value.

  • D) Use DefaultValidator in rules() to set a default attribute value during validation like so:

    public function rules() {
        return [
            [
                'attr', 'default',
                'value' => 'defaultValue',
                'on' => 'insert', // instantiate model with this scenario
            ],
            // ...
        ];
    }
    

    Problem: Same as B) and C). Value is not set until we actually save or validate the record.

So what is the right way to set default attribute values? Is there any other way without the outlined problems?

mae
  • 14,947
  • 8
  • 32
  • 47
  • $model = new DatabaseTable(); that's all you need, $model now has all the default attributes from the database table – Coz Jan 18 '17 at 03:36
  • @Coz Nope, that won't work for any dynamic attributes such as the current user's ID. – mae Jan 18 '17 at 04:31
  • Yeah it will, just use a custom callback. That said though if you want the current users id then use http://www.yiiframework.com/doc-2.0/yii-behaviors-blameablebehavior.html instead. – Coz Jan 18 '17 at 16:18
  • 1
    As you probably found out by yourself, behaviors, including the one you linked, only set the default value before *inserting* or *updating* the record. They are not the right answer as we need the value much earlier - on *initialization* (see option **B**). – mae Jan 19 '17 at 05:47
  • 1
    Description of the [DefaultValidator](https://www.yiiframework.com/doc/api/2.0/yii-validators-defaultvaluevalidator) said that it is not actual validatior. So if your create rule for default values you also should remove these fields from `required` rule! After that model will contain default values before validation/save calls. – Pavel Sokolov May 11 '19 at 20:00

7 Answers7

29

There's two ways to do this.

$model => new Model();

Now $model has all the default attributes from the database table.

Or in your rules you can use:

[['field_name'], 'default', 'value'=> $defaultValue],

Now $model will always be created with the default values you specified.

You can see a full list of core validators here http://www.yiiframework.com/doc-2.0/guide-tutorial-core-validators.html

Coz
  • 1,875
  • 23
  • 21
  • 8
    As I pointed out in option **D**, this solution is insufficient, because the value is not actually set until `$model->validate()` is executed. – mae Jan 18 '17 at 04:16
  • 1
    Regarding your first part of the question, you need to invoke `$model->loadDefaultValues()` for getting default attributes from the database table https://www.yiiframework.com/doc/guide/2.0/en/db-active-record#default-attribute-values. Please consider updating your answer – antoniom Feb 21 '23 at 08:47
17

This is a hangup with Yii's bloated multi-purpose ActiveRecords

In my humble opinion the form models, active records, and search models would be better off split into separate classes/subclasses

Why not split your search models and form models?

abstract class Creature extends ActiveRecord {
    ...
}

class CreatureForm extends Creature {

    public function init() {
        parent::init();
        if ($this->isNewRecord) {
            $this->number_of_legs = 4;
        }
    }
}

class CreatureSearch extends Creature {

    public function search() {
        ...
    }
}

The benefits of this approach are

  • You can easily cater for different validation, set up and display cases without resorting to a bunch of ifs and switches
  • You can still keep common code in the parent class to avoid repetition
  • You can make changes to each subclass without worrying about how it will affect the other
  • The individual classes don't need to know about the existence of any of their siblings/children to function correctly

In fact, in our most recent project, we are using search models that don't extend from the related ActiveRecord at all

Arth
  • 12,789
  • 5
  • 37
  • 69
  • 3
    It's an old question. This is almost exactly how I've been doing it since. Should be documented like this in Yii. – mae Mar 13 '19 at 20:24
  • 1
    Bad solution. Search model would have default values - grid view would be filtered by default with default values. – Anton Rybalko Apr 26 '19 at 13:57
  • @AntonRybalko I don't understand your comment.. the default values are set in CreatureForm (used to manipulate records) and NOT set in CreatureSearch (used for the GridView). So the GridView would NOT be filtered by default with default values. – Arth Apr 26 '19 at 15:23
  • 1
    Oops, sorry. Didn't notice it. – Anton Rybalko May 02 '19 at 14:51
6

I know it is answered but I will add my approach. I have Application and ApplicationSearch models. In Application model I add init with a check of the current instance. If its ApplicationSearch I skip initializations.

    public function init()
    { 
        if(!$this instanceof ApplicationSearch)  
        {
            $this->id = hash('sha256',  123);
        }

        parent::init();
    }

also as @mae commented below you can check for existence of search method in current instance, assuming you didn't add any method with name search to the non-search base model so the code becomes:

    public function init()
    { 
        // no search method is available in Gii generated Non search class
        if(!method_exists($this,'search'))  
        {
            $this->id = hash('sha256',  123);
        }

        parent::init();
    }
mae
  • 14,947
  • 8
  • 32
  • 47
Stefano Mtangoo
  • 6,017
  • 6
  • 47
  • 93
  • 1
    I like this best and it gave me an even better idea. Check for `!method_exists($this,'search')` instead so you don't have to know the child class name. – mae Jan 18 '17 at 04:27
  • 2
    I'm sorry, I really don't like this approach.. you are introducing a dependency on the child class from the parent. The child should be able to be changed willy-nilly without affecting the functionality of the parent, and any functionality specific to the child should be visible in the child. Checking for a method named 'search' is even worse! – Arth Mar 13 '19 at 17:14
  • @Arth, given the question constraints what is your proposed solution? – Stefano Mtangoo Mar 13 '19 at 18:54
  • I have added an answer – Arth Mar 14 '19 at 00:48
3

I've read your question several times and I think there are some contradictions.
You want the defaults to be readable before and during validation and then you try init() or beforeSave(). So, assuming you just want to set the default values in the model so they can be present during the part of the life cycle as long as possible and not interfere with the derived classes, simply set them after initialising the object.

You can prepare separate method where all defaults are set and call it explicitly.

$model = new Model;
$model->setDefaultValues();

Or you can create static method to create model with all default values set and return the instance of it.

$model = Model::createNew();

Or you can pass default values to constructor.

$model = new Model([
    'attribute1' => 'value1',
    'attribute2' => 'value2',
]);

This is not much different from setting the attributes directly.

$model = new Model;
$model->attribute1 = 'value1';
$model->attribute2 = 'value2';

Everything depends on how much transparent would you like your model be to your controller.

This way attributes are set for the whole life cycle except the direct initialisation and it's not interfering with derived search model.

Bizley
  • 17,392
  • 5
  • 49
  • 59
  • 1
    It's good to use `Yii::createObject` instead of `new` keyword. – meysam Sep 04 '16 at 08:19
  • Why? `new` is so much faster. – Bizley Sep 04 '16 at 08:21
  • `Yii::createObject` lets you do more than new. Also in my opinion it saves the semantic schema of Yii2. Other users can gain more understanding from you code, because all initializations are in one place, neither in different places nor different methods. – meysam Sep 04 '16 at 08:24
  • 2
    It's true, it's for DI container but in here it is overkill for such a simple case. Yii 2 core is not using this in every case as well - it affects performance. – Bizley Sep 04 '16 at 08:30
  • You understood my question correctly, but unfortunately the solutions you propose just aren't usable in the real world. The statement `(new MyModel)->attr` needs to return the default `attr` value. The whole point of default attribute values is that we don't have to manually set them every time. – mae Sep 04 '16 at 11:26
  • So what about first two methods? – Bizley Sep 04 '16 at 11:28
  • We don't always have control over the initialization so we have to make it work on `new MyModel` without any additional calls. Your second method is a bit better, but still requires a custom call. However reading your answer gave me a new idea. How about we use my method A) and add an additional class name check so that it only applies to the parent class, but not its children? – mae Sep 04 '16 at 11:36
  • Not really good because what if you want to extend the class so it gets default values but is not the search class? – Bizley Sep 04 '16 at 11:42
  • Indeed. Perhaps we should somehow *exclude* the search child class from acquiring the defaults (which is exactly what Yii1.1 did with `unsetAttributes()`) – mae Sep 04 '16 at 11:46
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/122609/discussion-between-user1132363-and-bizley). – mae Sep 04 '16 at 11:52
  • The method is now called `loadDefaultValues()` in Yii2. – Razvan Grigore Dec 06 '19 at 19:20
2

Just override __construct() method in your model like this:

class MyModel extends \yii\db\ActiveRecord {

    function __construct(array $config = [])
       {
           parent::__construct($config);
           $this->attr = 'defaultValue';
       }
    ...
}
0

If you want to load default value from database you can put this code in your model

 public function init()
 {
    parent::init(); 
    if(!method_exists($this,'search')) //for checking this code is on model search or not
    {
        $this->loadDefaultValues();
    }
 }
Dedy Kurniawan
  • 286
  • 2
  • 3
  • 9
-1

You can prepare separate method where all defaults are set and call it explicitly.

$model = new Model;
if($model->isNewRecord())
    $model->setDefaultValues();