📜 ⬆️ ⬇️

ActiveRecord's inheritance describing one table (single table inheritance pattern) in Yii2

In most relational databases, unfortunately, there is no support for inheritance, so you have to implement it manually. In this article I want to briefly show how to implement this approach to inheritance, as the “single table inheritance” described in the book “Patterns of Enterprise Application Architecture” by Martin Fowler.

In accordance with this pattern, you need to use a common table for inherited models and in this table add a type field that will define the class that inherits this record.

This article will use the following model inheritance structure:
')
 Car |- SportCar |- HeavyCar 

The `car` has the following structure:

 CREATE TABLE `car` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `type` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ); INSERT INTO `car` (`id`, `name`, `type`) VALUES (1, 'Kamaz', 'heavy'), (2, 'Ferrari', 'sport'), (3, 'BMW', 'city'); 

Car model can be generated with Gii.

How it works


We need a simple query class CarQuery , which will automatically substitute the type of car.

 namespace app\models; use yii\db\ActiveQuery; class CarQuery extends ActiveQuery { public $type; public function prepare($builder) { if ($this->type !== null) { $this->andWhere(['type' => $this->type]); } return parent::prepare($builder); } } 

And now we can create classes derived from Car . In them, we define the TYPE constant that will store the type of car for writing in the type field of the model, and override the ActiveRecord methods of init , find and beforeSave , in which this type will be automatically inserted into the model and into the CarQuery query. TYPE does not have to be a string (it is wiser to use an unsigned int), and not even necessarily a constant, but for simplicity, let's do this. This will be SportCar :

 namespace app\models; class SportCar extends Car { const TYPE = 'sport'; public function init() { $this->type = self::TYPE; parent::init(); } public static function find() { return new CarQuery(get_called_class(), ['type' => self::TYPE]); } public function beforeSave($insert) { $this->type = self::TYPE; return parent::beforeSave($insert); } } 

And so HeavyCar :

 namespace app\models; class HeavyCar extends Car { const TYPE = 'heavy'; public function init() { $this->type = self::TYPE; parent::init(); } public static function find() { return new CarQuery(get_called_class(), ['type' => self::TYPE]); } public function beforeSave($insert) { $this->type = self::TYPE; return parent::beforeSave($insert); } } 

Duplication of the code can be avoided by taking out these methods to the Car class and using the Car::getType method instead of the protected constant, but now I will not dwell on this for simplicity.

Now we need to override the Car:instantiate: method to automatically create the model of the desired class, depending on the type:

 public static function instantiate($row) { switch ($row['type']) { case SportCar::TYPE: return new SportCar(); case HeavyCar::TYPE: return new HeavyCar(); default: return new self; } } 

Being aware of all the heirs of the switch case in the code of the parent model is actually not a very good solution, but, again, this is done only for ease of understanding the approach and it is easy to get rid of it by slightly complicating the code.

Now for single table inheritance everything is ready. Here is a simple example of its transparent use in the controller:

 // finding all cars we have $cars = Car::find()->all(); foreach ($cars as $car) { echo "$car->id $car->name " . get_class($car) . "<br />"; } // finding any sport car $sportCar = SportCar::find()->limit(1)->one(); echo "$sportCar->id $sportCar->name " . get_class($sportCar) . "<br />"; 

This code will output the following:

 1 Kamaz app\models\HeavyCar 2 Ferrari app\models\SportCar 3 BMW app\models\Car 2 Ferrari app\models\SportCar 

As you can see, the models receive a class according to the type indicated by them.

Processing unique values


If there are fields in the table that are marked as unique in the model, in order for UniqueValidator skip them from different classes, you can use such a pleasant Yii chip as the targetClass :

  public function rules() { return [ [['MyUniqueColumnName'], 'unique', 'targetClass' => Car::classname()], ]; } 


This is a free translation of one of the useful “recipes” for Yii2, written by SamDark habravchanin here - https://github.com/samdark/yii2-cookbook , so if this article helped you with something, send rays of good - to him, and if did not like it, then the rays of evil to me.

Source: https://habr.com/ru/post/274925/


All Articles