[Из песочницы] Наследование ActiveRecord's, описывающих одну таблицу (паттерн single table inheritance) в Yii2

В большинстве реляционных баз данных, к сожалению, нет поддержки наследования, так что приходится реализовывать это вручную. В этой статье я хочу кратко показать, как реализовать такой подход к наследованию, как «single table inheritance», описанный в книге «Patterns of Enterprise Application Architecture» by Martin Fowler.

В соответствии с этим паттерном, нужно использовать общую таблицу для наследуемых моделей и в этой таблице добавить поле type, которое будет определять класс-наследника этой записи.

В этой статье будет использоваться следующая структура наследования моделей:

Car
|- SportCar
|- HeavyCar


Таблица `car` имеет следующую структуру:

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 можно сгенерировать с помощью Gii.

Как это работает


Нам понадобится простой класс запроса CarQuery, который автоматически будет подставлять тип автомобиля.

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);
    }
}


И теперь мы можем создать классы-наследники от Car. В них мы определим константу TYPE которая будет хранить тип автомобиля для записи в поле type модели, и переопределим ActiveRecord-методы init, find и beforeSave, в которых этот тип будет автоматически подставляться в модель и в запрос CarQuery. TYPE не обязательно должен быть строкой (разумнее использовать unsigned int) и даже не обязательно константой, но для простоты сделаем так. Таким будет 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);
    }
}


И таким 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);
    }
}


Дублирования кода, можно избежать, вынеся эти методы в класс Car и используя вместо константы protected метод Car::getType, но сейчас я не буду на этом останавливаться для простоты.

Теперь нам нужно переопределить метод Car:instantiate: для автоматического создания модели нужного класса, в зависимости от типа:

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


Знающий о всех наследниках switch case в коде модели-родителя — на самом деле не слишком удачное решение, но, опять же, это сделано только для простоты понимания подхода и от этого несложно избавиться чуть усложнив код.

Теперь для single table inheritance всё готово. Вот простой пример его прозрачного использования в контроллере:

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


Этот код выведет следующее:

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


Как можно заметить, модели получают класс в соответствии с указанным у них типом.

Обработка уникальных значений


Если в таблице есть поля, отмеченные в модели как уникальные, для того чтобы UniqueValidator пропускал их у разных классов, можно использовать такую приятную фишку Yii как targetClass:

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

© Habrahabr.ru