Attach behavior globally for all models in application config (without inheritance)
Asked Answered
S

2

9

I'm dealing with a problem for Yii2 on adding TimestampBehavior to run from main config. The reason is that i have to use it in frontend and backend on most of my models.

To use it in a model is simple :

public function behaviors()
{
    return [
        [
            'class' => TimestampBehavior::className(),
            'createdAtAttribute' => 'created_at',
            'updatedAtAttribute' => 'updated_at',
            'value' => function(){ return date('Y-m-d H:i:s'); } ,
        ],
    ];
}

But if i'm trying to add the behavior in main.php nothing happens. I was thinking at :

'as timestamp'=>[
    'class'=> \yii\behaviors\TimestampBehavior::className(),
    'createdAtAttribute' => 'created_at',
    'updatedAtAttribute' => 'updated_at',
    'value' => function(){ return date('Y-m-d H:i:s'); } ,
],

But it doesn't work. Any ideas?

Sternum answered 27/1, 2015 at 13:38 Comment(14)
Is it essentially for you do it in config?Sheepwalk
I can do it anywhere else, but i don't see where. I don't want to do a model with the behavior, and then extend all the models from this oneSternum
@Sternum Why not? It is the right way to do it.Saul
@Saul i think it seems legit for this to be in config. What i want to cancel the behavior, than i will have just an empty modelSternum
And how you want to determine models with attached TimestampBehavior? Manually or there is some kind of regularity?Sheepwalk
@Sternum sure, it can be, but in my opinion BaseModel/BaseController being empty at some certain point is not a big deal. I would use inheritance (imho).Saul
@Sheepwalk Most of my models, about 95% of them have those created_at and update_at fields. As far as i know, if a have a Timestamp Behavior attached to a model i will try to save those fields. I haven't tested the case if i don't have those fields what happens, but i will do it right nowSternum
@Sternum still, your approach is interesting, so I will follow the thread to see if somebody has a solutionSaul
@Sheepwalk i do get an error if i'm using TimestampBehavior and i don't have those fields in my database. The i guess that i should create my on behavior, to extend TimestampBehavior to check for those fields. Right?Sternum
I think it's kind of superfluous, you save on one thing, but lose on another one. Personally I recommend inheritance to solve this. I have another idea, but not sure about that right now.Sheepwalk
@Sheepwalk what idea?Sternum
I will write it as an answer after some testing in case everything will work as expected. Using base controller is also not an option for you since you need to extend all controllers from it?Sheepwalk
@Sheepwalk yes, not a very good ideea, since i will have to extend all my controllers, and there are a lot of them. My app is not at her beginings :) Also i can make my own general behavoir to work for this, and from main config, but i couldn't find the right solution yet to do it.Sternum
@Sheepwalk I can make an event in a new behavior like : ``` public function init() { parent::init(); ModelEvent::on(ActiveRecord::className(), BaseActiveRecord::EVENT_BEFORE_INSERT, function($event){ $this->attributes = [ BaseActiveRecord::EVENT_BEFORE_INSERT => [$this->createdAtAttribute, $this->updatedAtAttribute], ]; }); } ```Sternum
S
13

In case you don't want to use inheritance I can suggest the following method.

The basic idea behind it is using events of ActiveRecord:

use yii\base\Event;
use yii\behaviors\TimestampBehavior;
use yii\db\ActiveRecord;

...

$events = [ActiveRecord::EVENT_BEFORE_INSERT, ActiveRecord::EVENT_BEFORE_UPDATE];

foreach ($events as $eventName) {
    Event::on(ActiveRecord::className(), $eventName, function ($event) {
        $model = $event->sender;

        if ($model->hasAttribute('created_at') && $model->hasAttribute('updated_at')) {
            $model->attachBehavior('timestamp', [
                'class' => TimestampBehavior::className(),
                'value' => function () {
                    return date('Y-m-d H:i:s');
                },
            ]);
        }
    });
}

This code will dynamically attach TimestampBehavior to all models which are inherited from yii\db\ActiveRecord before saving it to database.

You can also omit createdAtAttribute and updatedAtAttribute because they already have these names by default (since it's most common).

As you can see behavior is attached only when both created_at and updated_at attributes exist, no need to create extended behavior for that.

To avoid inheritance and copy / paste this code should run on every application bootstrap.

You can add this to entry script (before application run) right away and it will work, but it's not good practice to place it here, also these files are generated automatically and in git ignored files list.

So you need to create just one separate component containing that logic and include it in the config. No need to extend classes etc.

Let's say it's called common\components\EventBootstrap. It must implement BootstrapInterface in order to work properly.

namespace common\components;

// Other namespaces from previous code

use yii\base\BootstrapInterface;

class EventBootstrap implements BootstrapInterface
{
    public function bootstrap($app)
    {
        // Put the code above here  
    }
}

Then you need to include it in config in bootstrap section:

return [
    'bootstrap' => [
        'common\components\EventBootstrap',
    ],
];

Official documentation:

Additional notes: I also tried to specify it through the application config only, but with no success.

I didn't find a way to specify ActiveRecord there.

You can see this question, but behavior there is attached to the whole application which is possible through config.

Sheepwalk answered 27/1, 2015 at 17:2 Comment(6)
Your solution is great! I had to do some modifications: namespace should be above use, and property_exists is checking for a public variable in the model, so it should be declared as public $created not public $created_at because with $created_at it will save nullSternum
please edit your answer so i can mark it as solution. Thanks a lot for your help. And one more thing, please make a correction from boostrap to bootstrap everywhereSternum
Also it can be used EVENT_BEFORE_VALIDATE not to overload the init procces, and to run only on validation, wich is a before save eventSternum
Glad to help. Corrected answer according to your corrections. As for events, I think using events EVENT_BEFORE_INSERT and EVENT_BEFORE_UPDATE is even more flexible. Sometimes you need to validate model without saving it. Also EVENT_AFTER_VALIDATE is event of Model, not ActiveRecord and you can have for example form models that you don't need to validate.Sheepwalk
I mean for form models you need validation, but don't need attaching TimestampBehavior (or even register event handler for checking and attaching it). Anyways you can always change the events depending on you requirements.Sheepwalk
EVENT_BEFORE_INSERT and EVENT_BEFORE_UPDATE weren't working for me. I had to use EVENT_AFTER_VALIDATE.Dreyer
N
4

I know its a bit old question but in case one is in same problem, I will share what I have found as clean way to do it. I just define a behavior in a trait. I then just use that trait in models and that is it. It adds only one line in model, yet its cleaner and makes code maintenance a bit breeze.

Here is sample trait

<?php

namespace common\traits;

use Yii;
use yii\behaviors\BlameableBehavior;
use yii\behaviors\TimestampBehavior;

trait Signature 
{

    public function behaviors()
    {
        return [
            [
                'class' => BlameableBehavior::className(),
                'createdByAttribute' => 'created_by',
                'updatedByAttribute' => 'updated_by',
            ],
            [
                'class' => TimestampBehavior::className(),
                'createdAtAttribute' => 'created_at',
                'updatedAtAttribute' => 'updated_at',
                'value' =>function(){
                    return time();
                },
            ],
        ];
    }
}

And model code

class Category extends \yii\db\ActiveRecord
{
    use \common\traits\Signature;
    // Model code here
}
Nenitanenney answered 4/7, 2016 at 18:1 Comment(3)
this looks scary and modern .. but is actually supported from PHP 5.4+Ecosphere
if you define function behaviors in Category .. it will override the Signature trait. Looks good but needs some more flexibilityEcosphere
You can suggest a better version. And yes, traits are recent but PHP >= 5.4 is quiet a standard right now :)Nenitanenney

© 2022 - 2024 — McMap. All rights reserved.