В соответствии с этим паттерном, нужно использовать общую таблицу для наследуемых моделей и в этой таблице добавить поле
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) . "<br />";
}
// finding any sport car
$sportCar = SportCar::find()->limit(1)->one();
echo "$sportCar->id $sportCar->name " . get_class($sportCar) . "<br />";
Этот код выведет следующее:
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()],
];
}
Это вольный перевод одного из полезных «рецептов» для Yii2, написанных хабравчанином SamDark здесь — https://github.com/samdark/yii2-cookbook, так что если эта статья чем-то вам помогла, отправляйте лучики добра — ему, а если не понравилась, то лучи зла мне.
Комментарии (13)
SamDark
12.01.2016 15:42+8Стоило упомянуть, что это перевод написанного мной одного из рецептов для Yii 2.0 Cookbook. За перевод спасибо.
Tairesh
12.01.2016 18:04Из песочницы нельзя отметить публикацию как перевод, ссылку добавил. Огромное вам спасибо за замечательные рецепты! Не думал, что автор русскоязычный, да ещё и есть на хабре.
SamDark
12.01.2016 18:15Если ещё переводить будете, имейте ввиду, что степень готовности там разная. Если у этот рецепт готов на 99%, то некоторые ещё совсем черновики.
andrewnester
12.01.2016 21:42просто как мнение — может быть интереснее и правильнее было бы сделать этот пример, показывая, что модели эти могут иметь разные поля и как этим всем управлять
SamDark
13.01.2016 00:47+1Разные — это, скорее всего, не Single Table Inheritance, а что-то типа Class Table Inheritance. Просто потому как хранить поля для такого в STI не оптимально.
Zazza
12.01.2016 20:22Из названия статьи показалось, что в yii2, точнее в его AR есть реализация STI. Но нет.
А за статью спасибо, я реализовывал наследование в моделях более хардко(р)но :)
Xu4
12.01.2016 21:24+2Дублирования кода, можно избежать, вынеся эти методы в класс Car и используя вместо константы protected метод Car::getType, но сейчас я не буду на этом останавливаться для простоты.
Можно и не использовать. Достаточно будет описать эти методы в классе Car таким образом:
class Car extends ActiveRecord { const TYPE = 'car'; public function init() { $this->type = static::TYPE; parent::init(); } public static function find() { return new CarQuery(get_called_class(), ['type' => static::TYPE]); } public function beforeSave($insert) { $this->type = static::TYPE; return parent::beforeSave($insert); } }
Ключевое словоstatic
как раз будет указывать, что речь идёт о том классе, который наследует логику, а не о том классе, в котором эта логика прописана (как происходит при использованииself
). Соответственно, если классHeavyCar
расширяет классCar
, то при вызове методаHeavyCar::init()
инструкция$this->type = static::TYPE;
будет, по сути, исполняться как$this->type = HeavyCar::TYPE;
, а при вызовеSportCar::init()
будет исполняться как$this->type = SportCar::TYPE;
. При этом, если в дочернем классе нет константыTYPE
, то она будет искаться в родительском классе. Вот пример, как работаетstatic
:
class ParentClass { const TYPE = 'parent'; public function getType() { return static::TYPE; } } class ChildOne extends ParentClass { const TYPE = 'child one'; } class ChildTwo extends ParentClass {} $instance = new ParentClass(); echo $instance->getType() . PHP_EOL; // parent $instance = new ChildOne(); echo $instance->getType() . PHP_EOL; // child one $instance = new ChildTwo(); echo $instance->getType() . PHP_EOL; // parent
А с самим подходом, когда для работы с одной и той же таблицей используются несколько моделей, я не согласен, потому что появится соблазн ввести в таблицеCar
поле, хранящее JSON-кодированную (или ещё каким-нибудь способом сериализованную) структуру данных с информацией о прицепах дляHeavyCar
. В случае с СУБД, которые поддерживают JSON это может быть нормально, но если используется MySQL, как это часто бывает, лучше всё-таки подумать над формализацией данных на уровне БД, а не на уровне кода.Xu4
12.01.2016 21:31+2Я уточню на всякий случай про
HeavyCar::init()
, который выглядит так, будто я пытаюсь вызывать статический метод init, хотя он при этом не статический. В PHP на письме принято методы класса показывать именно таким образом — вне зависимости от того, статичный метод или нет. Просто так принято писать. В официальной документации, например, так и делается. В самом коде, конечно же, я бы использовал коррекный способ вызова в зависимости от статичности метода.
Enkin
13.01.2016 14:00Буквально вчера читал про универсальные модули комментариев для YII2. Универсальные — в том смысле что один и тот же объект комментария может быть привязан к любому другому объекту (пост в блог, товар в каталоге и т.п.) одинаковым образом. Там одним из ключевых вопросов была уникальность идентификаторов объектов (id поста и id товара могут совпадать). Вот тут что-то похожее буду пробовать освоить в контексте этой задачи. Спасибо.
alpust
14.01.2016 20:06+1А если еще, разделить Модель на Объект поведения/состояния и репозиторий, то и вовсе можно будет паттерн называть Domain model pattern / Repository pattern :D
SilverFire
О черезмерно толстых моделях писал nepster09 в своей недавней статье Разработка приложений на Yii2 без опыта — прямой путь в АД, но по моему мнению, проблема сводится к сугубо архитектурной и решается очень аккуратно, хороший пример Tairesh и показал.
, но создание машин через этот класс невозможно, обязательно использовать уточненные классы. Это хорошо, когда каждый тип гарантированно имеет что-то свое, чего принципиально не может быть у базового и не пересекается с другими. Например, у спортивной машины, наверняка, не будет сцепного устройства для прицепа :)Такая структура моделей может быть особенно полезной, когда разные типы одного объекта требуют разных правил валидации или специфического поведения. Описывание особенностей разных типов одной модели в одном классе быстро превращает его в сложноподдерживаемое месиво. А если еще и добавить сценарии — можно плакать. Проверено :)
Что касается практики, я бы добавил, что создание объектов базового класса
Car
не всегда хорошая идея. Я вижу две стратегии:1)
Car
— суперкласс. Есть базовые правила валидации того, что есть у всех, типа2)
Car
— общий класс машины. Применим, когда удовлетворяет требованиям для создания большинства записей, но лишь некоторые типы содержат что-то специфической и требуют отдельных правил, которые будут вынесены в отдельный класс.Открытым может оставаться вопрос о правильном наследовании правил валидации и возможностям комбинирования похожих свойств, но
это может сильно отличатся в разных предметных областях.