PHP
Article

Yii 2.0 ActiveRecord Explained

By Arno Slatius

The ActiveRecord class in Yii provides an object oriented interface (aka ORM) for accessing database stored data. Similar structures can be found in most modern frameworks like Laravel, CodeIgniter, Smyfony and Ruby. Today, we’ll go over the implementation in Yii 2.0 and I’ll show you some of the more advanced features of it.

Database and networking concept

Model class intro

The Yii ActiveRecord is an advanced version of the base yii\base\Modelwhich is the foundation of the Model-View-Controller architecture. I’ll quickly explain the most important functionality that ActiveRecord inherits from the Model class:

Attributes

The business data is held in attributes. These are publicly available properties of the model instance.
All the attributes can conveniently be assigned massively by assigning any array to the attributes property of a model. This works because the base Component class (the base of almost everything in Yii 2.0) implements the __set() method which in turn calls the setAttributes() method in the Model class. The same goes for retrieving; all attributes can be retrieved by getting the attributes property. Again, built upon the Component class which implements __get() which calls the getAttributes() in the Model class.
Models also supply attribute labels which are used for display purposes which makes using them in forms on pages easier.

Validation

Data passed in the model from user input should be checked to see that they satisfy your business logic. This is done by specifying rules() which would normally hold one or more validators for each attribute.
By default, only attributes which are considered ‘safe’, meaning they have a validation rule defined for them, can be assigned massively.

Scenarios

The scenarios allow you to to define different ‘scenarios’ in which you’ll use a model allowing you to change the way it validates and handles its data. The example in the documentation describes how you can use it in a FormModel (which also extends the Model class) by specifying different validation rules for both user registration and login simply by defining two different scenarios in one Model.

Creating an ActiveRecord model

An ActiveRecord instance represents a row in a database table, therefore we need a database. In this article I’ll use the database design in the picture below as as an example. It’s a simple structure for blog articles. Authors can have multiple articles which can have multiple tags. The articles are related through an N:M relation to the tags because we want to be able to show related articles based on the tags. The ‘articles’ table will get our focus because it has the most interesting set of relations.

db_structure.PNG

I’ve used the Gii extension to generate the model based on the table and it’s relations. Here’s what is generated for the articles table from the database structure just by clicking a few buttons:

namespace app\models;

use Yii;

/**
 * This is the model class for table "articles".
 *
 * @property integer $ID
 * @property integer $AuthorsID
 * @property string $LastEdited
 * @property string $Published
 * @property string $Title
 * @property string $Description
 * @property string $Content
 * @property string $Format
 *
 * @property Authors $authors
 * @property Articlestags[] $articlestags
 */
class Articles extends \yii\db\ActiveRecord
{
    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'articles';
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [['AuthorsID'], 'integer'],
            [['LastEdited', 'Title', 'Description', 'Content'], 'required'],
            [['LastEdited', 'Published'], 'safe'],
            [['Description', 'Content'], 'string'],
            [['Format'], 'in', 'range' => ['MD', 'HTML']],
            [['Title'], 'string', 'max' => 250],
        ];
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'ID' => Yii::t('app', 'ID'),
            'AuthorsID' => Yii::t('app', 'Authors ID'),
            'LastEdited' => Yii::t('app', 'Last Edited'),
            'Published' => Yii::t('app', 'Published'),
            'Title' => Yii::t('app', 'Title'),
            'Description' => Yii::t('app', 'Description'),
            'Content' => Yii::t('app', 'Content'),
            'Format' => Yii::t('app', 'Format'),
        ];
    }

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getAuthors()
    {
        return $this->hasOne(Authors::className(), ['ID' => 'AuthorsID']);
    }

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getArticlestags()
    {
        return $this->hasMany(Articlestags::className(), ['ArticlesID' => 'ID']);
    }
}

The properties listed in the comment before the class definition show which attributes are available on every instance. It is good to notice that because of the relation definitions (defined in this class), you also get properties for the related data; one Authors $authors and multiple Articlestags[] $articlestags.

The tableName() function defines which database table is related to this Model. This allows a decoupling of the class name from the actual table name.

The rules() define the validation rules for the model. There are no scenarios defined so there is only one set of rules. It’s quite readable; showing which fields are required and which require an integer or string. There are quite a few core validators available which suit most people’s needs.

The attributeLabels() function supplies labels to be shown for each attribute should it be used in views. I chose to make mine i18n compatible from Gii which added all the calls to Yii::t(). This basically means that translation of the labels, which end up in the rendered pages, will be much easier later on should we need it.
Finally, the getAuthors() and getArticlestags() functions define the relation of this table to other tables.

Note: I was quite surprised to find the ‘Format’ attribute was missing completely from the properties, validators and labels. Turns out that Gii doesn’t support ENUMs. Besides MySQL (and its forks) only PostgreSQL supports it and therefore it wasn’t implemented. I had to add them manually.

Completing the model

You can probably see that the generated Articles class only has relations defined for the foreign key tables. The N:M relation from Articles to Tags won’t be generated automatically so we’ll have to define that by hand.

The relations are all returned as instances of yii\db\ActiveQuery. To define a relation between Articles and Tags we’ll need to use the ArticlesTags as a junction table. In ActiveQuery, this is done by defining a via table. ActiveQuery has two methods you can use for this:

  • via() allows you to use an already defined relation in the class to define the relation.
  • viaTable() alternatively allows you to define a table to use for a relation.

The via() method allows you to use an already defined relation as via table. In our example, however, the ArticlesTags table holds no information that we care for so I’ll use the viaTable() method.

/**
 * @return \yii\db\ActiveQuery
 */
public function getTags()
{
    return $this->hasMany(Tags::className(), ['ID' => 'TagsID'])
			    ->viaTable(Articlestags::tableName(), ['ArticlesID' => 'ID']);
}

Now that we’ve defined all the relations, we can start using the model.

Using the model

I’ll quickly create some database content by hand.

$phpTag = new \app\models\Tags();		//Create a new tag 'PHP'
$phpTag->Tag = 'PHP';
$phpTag->save();

$yiiTag = new \app\models\Tags();		//Create a new tag 'Yii'
$yiiTag->Tag = 'Yii';
$yiiTag->save();

$author = new \app\models\Authors();	//Create a new author
$author->Name = 'Arno Slatius';
$author->save();

$article = new \app\models\Articles();	//Create an article and link it to the author
$article->AuthorsID = $author->ID;
$article->Title = 'Yii 2.0 ActiveRecord';
$article->Description = 'Arno Slatius dives into the Yii ActiveRecord class';
$article->Content = '... the article ...';
$article->LastEdited = new \yii\db\Expression('NOW()');
$article->save();

$tagArticle = new \app\models\ArticlesTags();	//Link the 'PHP' tag to the article
$tagArticle->ArticlesID = $article->ID;
$tagArticle->TagsID = $phpTag->ID;
$tagArticle->save();

$tagArticle = new \app\models\ArticlesTags();	//Link the 'Yii' tag to the article
$tagArticle->ArticlesID = $article->ID;
$tagArticle->TagsID = $yiiTag->ID;
$tagArticle->save();

It should be noted that the code above has some assumptions which I’ll explain;

  • The result of save(), a boolean, isn’t evaluated. This isn’t wise normally, because Yii will actually call validate() on the class before actually saving it in the database. The database INSERT won’t be executed should any of the validation rules fail.
  • You might notice that the ID attributes of the various instances are used while they are not set. This can be done safely because the save() call will INSERT the data and get assigned the primary key back from the database and make the ID property value valid.
  • The $article->LastEdited is a DateTime value in the database. I want to insert the current datetime by calling the NOW() SQL function on it. You can do this by using the Expression class which allows the usage of various SQL expressions with ActiveRecord instances.

You can then retrieve the article again from the database;

//Look up our latest article
$article = \app\models\Articles::findOne(['Title'=>'Yii 2.0 ActiveRecord']);

//Show the title
echo $article->Title;

//The related author, there is none or one because of the hasOne relation
if (isset($article->authors)) {
    echo $article->authors->name
}

//The related tags, always an array because of the hasMany relations
if (isset($article->tags)) {
    foreach($article->tags as $tag) {
        echo $tag->Tag;
    }
}

New and advanced usages

The Yii ActiveRecord, as I’ve described it so far, is straight forward. Let’s make it interesting and go into the new and changed functionality in Yii 2.0 a bit more.

Dirty attributes

Yii 2.0 introduced the ability to detect changed attributes. For ActiveRecord, these are called dirty attributes because they require a database update. This ability now by default allows you to see which attributes changed in a model and to act on that. When, for example, you’ve massively assigned all the attributes from a form POST you might want to get only the changed attributes:

//Get a attribute => value array for all changed values
$changedAttributes = $model->getDirtyAttributes();

//Boolean whether the specific attribute changed
$model->isAttributeChanged('someAttribute');

//Force an attribute to be marked as dirty to make sure the record is 
// updated in the database
$model->markAttributeDirty('someAttribute');

//Get on or all old attributes
$model->getOldAttribute('someAttribute');
$model->getOldAttributes();

Arrayable

The ActiveRecord, being extended from Model, now implements the \yii\base\Arrayable trait with it’s toArray() method. This allows you to convert the model with attributes to an array quickly. It also allows for some nice additions.

Normally a call to toArray() would call the fields() function and convert those to an array. The optional expand parameter of toArray() will additionally call extraFields() which dictates which fields will also be included.

These two fields methods are implemented by BaseActiveRecord and you can implement them in your own model to customize the output of the toArray() call.

I’d like, in my example, to have the extended array contain all the tags of an article available as a comma separated string in my array output as well;

public function extraFields()
{
	return [
		'tags'=>function() {
			if (isset($this->tags)) {
				$tags = [];
				foreach($this->tags as $articletag) {
					$tags[] = $articletag->Tag;
				}
				return implode(', ', $tags);
			}
		}
	];
}

And then get an array of all the fields and this extra field from the model;

//Returns all the attributes and the extra tags field
$article->toArray([], ['tags']);

Events

Yii 1.1 already implemented various events on the CActiveRecord and they’re still there in Yii 2.0. The ActiveRecord life cycle in the Yii 2.0 guide shows very nicely how all these events are fired when using an ActiveRecord. All the events are fired surrounding the normal actions of your ActiveRecord instance. The naming of the events is quite obvious so you should be able to figure out when they are fired; afterFind(), beforeValidate(), afterValidate(), beforeSave(), afterSave(), beforeDelete(), afterDelete().

In my example, the LastEdited attribute is a nice way to demonstrate the use of an event. I want to make sure LastEdited always reflects the last time the article was edited. I could set this on two events; beforeSave() and beforeValidate(). My model rules define LastEdited as a required attribute so we need to use the beforeValidate() event to make sure it is also set on new instances of the model;

public function beforeValidate($insert)
{
    if (parent::beforeValidate($insert)) {
		$this->LastEdited = new \yii\db\Expression('NOW()');
        return true;
    }
    return false;
}

Note that with all of these events, you should call the parent event handler. Returning false (or nothing!) from a before event in these functions stops the action from happening.

Behavior

Behavior can be used to enhance the functionality of an existing component without modifying its code. It can also respond to the events in the component that it was attached to. They behave similar to the traits introduced in PHP 5.4.
Yii 2.0 comes with a number of available behaviors;

  • yii\behaviors\AttributeBehavior allows you to specify attributes which need to be updated on a specified event. You can, for example, set an attribute to a value based on an unnamed function on a BEFORE_INSERT event.

  • yii\behaviors\BlameableBehavior does what you’d expect; blame someone. You can set two attributes; a createdByAttribute and updatedByAttribute which will be set to the current user ID when the object is created or updated.

  • yii\behaviors\SluggableBehavior allows you to automatically create a URL slug based on one of the attributes to another attribute in the model.

  • yii\behaviors\TimestampBehavior will allow you to automatically create and update the time stamp in a createdAtAttribute and updatedAtAttribute in your model.

You can probably see that these have some practical applications in my example as well. Assuming that the person currently logged in to the application is the actual author of an article, I could use the BlameableBehavior to make them the author and I can also use the TimestampBehaviour to make sure the LastEdited attribute stays up to date. This would replace my previous implementation of the beforeValidate() event in my model. This is how I attached the behaviors to my Articles model:

public function behaviors()
{
    return [
	    [
            'class' => \yii\behaviors\BlameableBehavior::className(),
            'createdByAttribute' => 'AuthorID',
            'updatedByAttribute' => 'AuthorID',
        ],
        [
            'class' => \yii\behaviors\TimestampBehavior::className(),
            'createdAtAttribute' => false,    //or 'LastEdited'
            'updatedAtAttribute' => 'LastEdited',
            'value' => new \yii\db\Expression\Expression('NOW()'),
        ],
    ];
}

I assume here of course that the creator and the editor of the article is the same person. Since I don’t have a created timestamp field, I chose not to use it by setting the createdAtAttribute to false. I could of course also set this to 'LastEdited'.

Transactional operations

The last feature I want to touch is the possibility to automatically force the usage of transactions in a model. With the enforcement of foreign keys also comes the possibility for database queries to fail because of that. This can be handled more gracefully by wrapping them in a transaction. Yii allows you to specify operations that should be transactional by implementing a transactions() function in your model that specifies which operations in which scenarios should be enclosed in a transaction. Note that you should return a rule for the SCENARIO_DEFAULT if you want this to be done by default on operations.

public function transactions()
{
	return [
		//always enclose updates in a transaction
	    \yii\base\Model::SCENARIO_DEFAULT => self::OP_UPDATE,	    
	    //include all operations in a transaction for the 'editor' scenario
	    'editor' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
	];
}

Conclusion

The Yii ActiveRecord class already made ORM handling very simple, Yii 2.0 builds upon this great base and extends it even further. The flexibility is huge due to the possibility to define different usage scenarios, attach behaviors and use events.

These are some features in the ActiveRecord that I’ve found most useful over time and most welcome with the arrival of Yii 2.0. Did you miss a feature of ActiveRecord, or perhaps feel that Yii ActiveRecord is missing a great feature from another framework? Please let us know in the comments!

Free Guide:

7 Habits of Successful CTOs

"What makes a great CTO?" Engineering skills? Business savvy? An innate tendency to channel a mythical creature (ahem, unicorn)? All of the above? Discover the top traits of the most successful CTOs in this free guide.

  • Samat Zhanbekov

    tell us about Yii2 unit testing please. Information about it is too small.

    • http://www.slatius.nl Arno Slatius

      Well Yii unit testing is based on CodeCeption so information on that is valid for Yii as well.
      But thanks for the suggestion, I’ll look into it. Anything specific you missing?

      • Guest

        I just discovered my error. I created my schema using MySQL-Workbench which created a compound primary key because of the relation being transitive (useless in my case). Removed this PK and it works. The link() method checks if all attributes for the primary key of the linked model are present (which wasn’t the case in my application). Now it works great.

      • Samat Zhanbekov

        Thanks. Well i’m starting to build Yii2 app on unit testing and it seems to me there is no full information about testing in this framework. It will be great if you show us complex unit testing tutorial in Yii2 app from A to Z: best practices.
        You’ll be first and it will be popular.

  • Vitor Silva

    I am really disappointed with the way Yii has evolved. I understand the benefits of adopting the PSR norms and namespaces …. but the code is extremely verbose now. That brings serious productivity and code readability issues.

    • http://www.slatius.nl Arno Slatius

      If you are refferring to the `yiipartclass` stuff then that’s my bad. A simple `use` statement of that namespace at the start of your class fixes that. I wrote full namespace classes here for readability, but that’s not normal practice.

  • Urkman

    Hi,

    Nice article…

    I don’t like the model generator, so I build my own. Take a look here, if you are interested:
    http://www.yiiframework.com/forum/index.php/topic/59609-abstract-model-generator/

    Greetings,
    Urkman

  • Hans Höchtl

    I was hoping you could explain the usage of the $model->link() method. I’m currently trying to figure out in which direction this should be used (parent to child-model or vice-versa). I got it working for something like this:
    Sales Contact (m:n)
    $sales = appmodelsSales::findOne([‘ident’ => ‘myident’]);
    $contact = new appmodelsContact();
    $contact->save();
    $contact->link(‘sales’,$sales); // Creates record in the m:n table

    But for 1:n relations I can’t get this to work.

    • http://www.slatius.nl Arno Slatius

      Thanks for this one! I hadn’t yet spotted this when I wrote the article and I’ve only just tried this. It works very nice indeed!
      The first parameter for the link() function is used in BaseActiveRecord->getRelation($name). So that means that your Contact class should implement a getSales() relation returning an ActiveQuery object.
      What does your Contact->getSales() relation definition look like?

      • Hans Höchtl

        I just discovered my error. I created my schema using MySQL-Workbench which created a compound primary key because of the relation being transitive (useless in my case). Removed this PK and it works. The link() method checks if all attributes for the primary key of the linked model are present (which wasn’t the case in my application). Now it works as expected.

  • ayaboy

    hi
    can you help me?
    i begin to make site on yii 1.
    i can’t to find function analogically “group by” of mysql

  • Dimitar Raev

    Sorry to be so picky but why why are you using capital letters for table fields / class properties …
    I thought Yii standard is to use CamelCase starting with LowerCase letter for properties and variables .. so they are not confused with Classes/ Objects. It is OK to do what you like on personal project but for tutorials it does not look good…
    ref: https://github.com/yiisoft/yii2/blob/master/docs/internals/core-code-style.md#42-properties

  • al8anp

    Nice article thanks !
    Small correction : beforeValidate() has no arguments, so
    `public function beforeValidate($insert)` should actually be `public function beforeValidate()`
    (the $insert arg is used in beforeSave)

    • http://www.slatius.nl Arno Slatius

      I stand corrected, thanks. Not sure how that got in there.

  • http://www.slatius.nl Arno Slatius

    I agree that one of the strong points of Yii is their definition of coding styles and rules.
    This difference in attribute naming however is based on what Gii generated from my table design and the way I design my tables indeed goes against the Yii style. I’ll try to think of it next time, thanks for pointing it out.

  • Yanmin Dai

    this tutorial does offer some helps. However i most dislike is the article_tags example. I believe the manual insert is not the best way. I am hoping to see something how to automated the process of saving to article_tag table in active record… not manually saving one by one

  • Horváth Péter

    Thanks Arno for the great article. I have a question though. As I can see, when I have a 5000 row table, when I use ActiveRecord with gridView, and I make it possible for the user to see all the 5000 row, this solution gets very slow. The reason is that the ActiveRecord queries the database one by one (I have multiple joins in the query). I have a query written which gets the whole thing in one query, and it is 10 times faster. Can I use this with GridView, or in this way it will always use the one by one queries? Thanks!

    • http://www.slatius.nl Arno Slatius

      From what you describing you might be better of with the SqlDataProvider; http://www.yiiframework.com/doc-2.0/yii-data-sqldataprovider.html. You might alternatively try and do some eager loading on the ActiveRecord by doing a Model::find()->with(‘relation’)->where(….)->all(); but that depends on how you do it now exactly.

      • Horváth Péter

        Thanks for your help. Eager loading has solved my issue.

        Happy New Year!! :)

  • Edward Ku

    Hi Arno ,Sorry to bother you ,Please give me a hand ,these formula cannot work ,I cannot find out where is the problem ,please help me ,thank you very much! which is followed :

    yii2.0

    function validateTel($attribute, $params)

    {

    if(empty($this->tel) && empty($attribute->mobile)){$this->addError($attribute->tel,’ERROR’);}

    else{$this->addError($attribute->tel,’OK’);}

    }

    public function rules()

    {

    public $tel;

    public $mobile;

    return

    [

    [[‘tel’,mobile],’validateTel’],

    ];

    }

  • Edward Ku

    Hi Arno ,Sorry to bother you ,Please give me a hand ,these formula cannot work ,I cannot find out where is the problem ,please help me ,thank you very much! which is followed :

    yii2.0

    function validateTel($attribute, $params)

    {

    if(empty($this->tel) && empty($attribute->mobile)){$this->addError($attribute->tel,’ERROR’);}

    else{$this->addError($attribute->tel,’OK’);}

    }

    public function rules()

    {

    public $tel;

    public $mobile;

    return

    [

    [[‘tel’,mobile],’validateTel’],

    ];

    }

  • Niloy

    public function beforeValidate($insert)….. here what is the $insert ??

  • Rajat Batra

    Hi ,

    In Yii 2 i am able to access value using object notation and using array notation as well without doing any change. Is it normal?

  • jkirkby

    The worst thing about YII is its active record, its far too complex for its own good, why write a class for every table? if you have 30 tables in your application do you seriously want to write 30 classes to interface with it? Codeigniters active record however is elegantly simple

    • http://www.slatius.nl Arno Slatius

      I’ll be honest to say that I’ve only glanced at CodeIgniter a long time ago so I really wouldn’t know. The only thing I can say in favor of Yii is that Gii makes it very easy to make a quick start with these models, it’ll generate them automatically based on available database tables.
      I feel that having a model for each table will make you think about data validation more quickly and also more on how to do data storage and how to properly structure it. I do believe this is where Yii differs from other frameworks; it defines a structure initially (from which you can deviate if you want to) which may seem over complicated at first but once your project becomes more complex you definitely benefit from that structure.

  • AJM

    this is phenomenal!… this has done more to explain how to use Yii2 in 10 min then reading the official documentation for a dozen hours. IMHO the documentation is critically missing practical examples. I would venture to say that Yii loses so many adopters of such a great platform because of this.

  • Gzhelin Alexey

    Thanks for the tutorial, i finally understood yii relations mechanism!

  • YII Developer

    Thanks i solved my last doubt.
    i need still modification can anyone help me out for that.
    assume that i have 250 page so i need to divide in some specific part like
    <<(FIRST) <(PREVS) …10(FIRST 10 PAGES) ..20 (11 TO 20 PAGES)..so on
    in 250 pages its little bit hard to move single singe page.

  • http://quran.2index.net/ Said Bakr

    I could able to retrieve all related tables fields like `SELECT * …` but how to get a specific fields from the relation?

  • Maksim Muruev

    How to map object like from JSON fields into one object with MyClassType or some fields combine from the table into one helping object for example I have Money and want combine currency and amont from table into this objec other fields leave as they are for main ActiveRecordObject.

  • pandian

    Plz one help,,
    i am yii beginer i want to the follow one result
    -> i am create one table in backend and its fill record allso
    ->now the records are how to diplay in fronend ( i am using find() conscept )

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in PHP, once a week, for free.