Let Yii2 Behaviors Own Your Audit Fields: TimestampBehavior and BlameableBehavior in Practice
Stop setting created_at and created_by in controllers. Yii2's TimestampBehavior and BlameableBehavior move audit fields into the model—here's the configuration, a worked example, and the query-builder bypass you need to know about.
20 Nov 2025, 03:56 UTC

Every Yii2 project eventually grows the same boilerplate: controllers setting created_at before insert, updated_at before update, and created_by from the logged-in user. It works until someone adds a console import command or a REST endpoint and forgets to copy the boilerplate. The cleaner engineering decision is to move that responsibility into the model itself using two behaviors that ship with Yii2: TimestampBehavior and BlameableBehavior.
The thesis is simple: audit fields are a property of the record, not of the request. Behaviors let the model enforce them no matter which code path saves the data.
How behaviors automate the fields
A behavior in Yii2 is a reusable object that attaches event handlers to a component. TimestampBehavior hooks the Active Record events beforeInsert and beforeUpdate and writes values into the attributes you name. BlameableBehavior does the same, but fills them with the current user's ID from the application's user component. Neither requires any controller code at all.
Worked example: an auditable Post model
Assume a post table with created_at, updated_at, created_by, and updated_by integer columns. In the model:
use yii\\db\\ActiveRecord;\nuse yii\\behaviors\\TimestampBehavior;\nuse yii\\behaviors\\BlameableBehavior;\n\nclass Post extends ActiveRecord\n{\n public function behaviors()\n {\n return [\n [\n 'class' => TimestampBehavior::class,\n 'createdAtAttribute' => 'created_at',\n 'updatedAtAttribute' => 'updated_at',\n 'value' => function () { return time(); },\n ],\n [\n 'class' => BlameableBehavior::class,\n 'createdByAttribute' => 'created_by',\n 'updatedByAttribute' => 'updated_by',\n ],\n ];\n }\n\n public function rules()\n {\n return [\n [['title', 'body'], 'required'],\n // Deliberately NO rule for created_at/updated_at/created_by/updated_by\n ];\n }\n}Two details matter here. First, the audit attributes are intentionally absent from rules(), so they are not \"safe\" for massive assignment — a crafted form submission cannot overwrite them. Second, the value callable can be swapped for a database expression, new \\yii\\db\\Expression('NOW()'), if you want the database server's clock to be authoritative. That avoids PHP/DB clock skew, at the cost of making the value harder to assert in unit tests.
Now any $post->save() — from a web controller, a console command, or a REST action — populates all four fields. Verify it by creating a record, checking the columns in the database, then updating the record and confirming created_at stays fixed while updated_at changes.
Trade-offs you should know before committing
- Query builder bypasses behaviors.
Yii::$app->db->createCommand()->update(...)orPost::updateAll(...)never fires Active Record events, so audit fields stay stale. Bulk updates need explicit column values. - BlameableBehavior needs a user. In console commands or guest flows there is no authenticated identity, so it stores null unless you override
valuewith a fallback (for example, a system account ID). - Implicit side effects. Event-driven magic is easy to forget when debugging. Keep the set of behaviors small and named; a model with ten behaviors is harder to reason about than one with two.
- Version assumptions. This applies to Yii2's behavior API. Yii 1.1 differs substantially, and Yii 3 restructures the packages. Check
vendor/yiisoft/yii2/behaviors/TimestampBehavior.phpin your installed version to confirm available options.
Actionable closing
If your project sets audit fields in more than one place, consolidate them into these two behaviors this week. Then run one deliberate test: perform an update through the query builder and observe that the timestamps do not change. That single check tells you exactly where the automation ends and where you still need discipline.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.