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 theinit()
method and assign default value whenisNewRecord
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 callingsearch()
in the derivedMySearchModel
class and interfere with searching (theattr
attribute will already be set so search will be returning incorrect results). In Yii1.1 this was resolved by callingunsetAttributes()
before callingsearch()
, however no such method exists in Yii2.B) In
MyModel
override thebeforeSave()
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
isnull
. Worse yet, even other validation rules that rely on this value will not be able to access it, becausebeforeSave()
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
inrules()
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?
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()
orbeforeSave()
. 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.
Or you can create static method to create model with all default values set and return the instance of it.
Or you can pass default values to constructor.
This is not much different from setting the attributes directly.
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.
You can prepare separate method where all defaults are set and call it explicitly.
There's two ways to do this.
Now
$model
has all the default attributes from the database table.Or in your rules you can use:
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
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?
The benefits of this approach are
In fact, in our most recent project, we are using search models that don't extend from the related ActiveRecord at all
Just override
__construct()
method in your model like this: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.
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: