Skip to content

Keep generated virtual attributes out of INSERT/UPDATE #117

@siggi-k

Description

@siggi-k

Virtual attributes break save() after refresh()

Follow-up to #116 (virtual attribute support, x-db-type: false).

Problem

A property declared with x-db-type: false is generated as a virtual attribute:

protected $virtualAttributes = ['canBeDeleted'];
public $canBeDeleted;

public function attributes()
{
    return array_merge(parent::attributes(), $this->virtualAttributes);
}

Because the virtual attribute is added to attributes(), Yii treats it as a column-backed attribute. After refresh() it ends up in the dirty set, and the next save() writes it to a column that does not exist:

SQLSTATE[42703]: Undefined column: column "canBeDeleted" of relation "documents" does not exist

Why (root cause)

yii\db\BaseActiveRecord::refreshInternal() iterates attributes() and forces every name (incl. virtual ones) into _attributes, while _oldAttributes comes from populateRecord() (real DB columns only) and therefore lacks them:

foreach ($this->attributes() as $name) {
    $this->_attributes[$name] = $record->_attributes[$name] ?? null; // virtual attr now set
}
$this->_oldAttributes = $record->_oldAttributes;                     // virtual attr NOT here

→ virtual attribute is in _attributes but not in _oldAttributes ⇒ counted as dirty.

insertInternal() / updateInternal() build the SQL from getDirtyAttributes(), so the virtual attribute gets written.

Note: a plain find() does not trigger this (populateRecord only sets real columns), so it stays latent until a refresh()-then-save() path is hit (e.g. updating a document during invoice billing). Every generated model with virtual attributes is affected.

What to do

Generated base models with virtual attributes must exclude them from DB writes. Add to the generated base:

public function getDirtyAttributes($names = null)
{
    return array_diff_key(parent::getDirtyAttributes($names), array_flip($this->virtualAttributes));
}

This keeps virtual attributes readable/serializable (they stay in attributes()) but guarantees they are never part of INSERT/UPDATE — independent of how they became dirty (find, refresh, setAttributes).

Repro

  1. Generate a model with an x-db-type: false property.
  2. $m = Model::findOne($id); $m->refresh(); $m->someRealColumn = 'x'; $m->save();
  3. Undefined column "<virtualAttribute>".

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions