Класс ActiveRecord в Yii предоставляет объектно-ориентированный интерфейс (или ORM) для доступа к данным, хранящимся в базе данных. Подобные структуры можно найти в большинстве современных сред, таких как Laravel, CodeIgniter, Smyfony и Ruby. Сегодня мы рассмотрим реализацию в Yii 2.0 и покажу вам некоторые его более продвинутые возможности.
Модель класса интро
Yii ActiveRecord — это расширенная версия base yii\base\Model
которая является основой архитектуры Model-View-Controller. Я быстро объясню наиболее важные функциональные возможности, которые ActiveRecord наследует от класса Model:
Атрибуты
Бизнес-данные хранятся в атрибутах. Это общедоступные свойства экземпляра модели.
Все атрибуты удобно назначать массово , назначая любой массив свойству attributes
модели. Это работает, потому что базовый класс Component (основа почти всего в Yii 2.0) реализует метод __set()
который, в свою очередь, вызывает метод setAttributes()
в классе Model. То же самое касается извлечения; все атрибуты можно получить, получив свойство attributes
. Опять же, построенный на классе Component, который реализует __get()
который вызывает getAttributes()
в классе Model.
Модели также предоставляют метки атрибутов, которые используются для отображения, что облегчает их использование в формах на страницах.
Проверка
Данные, передаваемые в модели из пользовательского ввода, следует проверить, чтобы убедиться, что они удовлетворяют вашей бизнес-логике. Это делается путем указания rules()
которое обычно содержит один или несколько валидаторов для каждого атрибута.
По умолчанию только массовые атрибуты могут быть назначены только тем атрибутам, которые считаются «безопасными», то есть для них определено правило проверки.
Сценарии
Сценарии позволяют вам определять различные «сценарии», в которых вы будете использовать модель, позволяющую изменить способ проверки и обработки данных. Пример в документации описывает, как вы можете использовать его в FormModel (который также расширяет класс Model), задав различные правила проверки как для регистрации пользователя, так и для входа в систему, просто определив два разных сценария в одной модели.
Создание модели ActiveRecord
Экземпляр ActiveRecord представляет строку в таблице базы данных, поэтому нам нужна база данных. В этой статье я буду использовать дизайн базы данных на рисунке ниже в качестве примера. Это простая структура для статей блога. Авторы могут иметь несколько статей, которые могут иметь несколько тегов. Статьи связаны через отношение N: M к тегам, потому что мы хотим иметь возможность отображать связанные статьи на основе тегов. Таблица «статей» получит наше внимание, потому что имеет наиболее интересный набор отношений.
Я использовал расширение Gii для генерации модели на основе таблицы и ее отношений. Вот что генерируется для таблицы статей из структуры базы данных, просто нажав несколько кнопок:
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']); } }
Свойства, перечисленные в комментарии перед определением класса, показывают, какие атрибуты доступны в каждом экземпляре. Приятно отметить, что из-за определений отношений (определенных в этом классе) вы также получаете свойства для связанных данных; один Authors $authors
и несколько Articlestags[] $articlestags
.
Функция tableName()
определяет, какая таблица базы данных связана с этой моделью. Это позволяет отделить имя класса от фактического имени таблицы.
rules()
определяют правила проверки для модели. Сценарии не определены, поэтому существует только один набор правил. Это вполне читабельно; показывая, какие поля являются обязательными, а какие — целым числом или строкой. Есть довольно много доступных валидаторов, которые удовлетворяют потребности большинства людей.
Функция attributeLabels()
предоставляет метки, которые должны отображаться для каждого атрибута, если он используется в представлениях. Я решил сделать мой i18n совместимым с Gii, который добавил все вызовы Yii::t()
. По сути, это означает, что перевод меток, которые заканчиваются на визуализированных страницах, будет намного легче в дальнейшем, если нам это понадобится.
Наконец, функции getAuthors()
и getArticlestags()
определяют отношение этой таблицы к другим таблицам.
Примечание . Я был очень удивлен, обнаружив, что атрибут «Формат» полностью отсутствует в свойствах, валидаторах и метках. Оказывается, что Gii не поддерживает ENUM. Помимо MySQL (и его форков) его поддерживает только PostgreSQL, и поэтому он не был реализован. Я должен был добавить их вручную.
Завершение модели
Возможно, вы видите, что сгенерированный класс Articles имеет отношения, определенные только для таблиц внешнего ключа. Отношение N: M от статей к тегам не будет сгенерировано автоматически, поэтому мы должны определить это вручную.
Все отношения возвращаются как экземпляры yii \ db \ ActiveQuery . Чтобы определить связь между статьями и тегами, нам понадобится использовать ArticlesTags в качестве таблицы соединений. В ActiveQuery это делается путем определения промежуточной таблицы . ActiveQuery имеет два метода, которые вы можете использовать для этого:
-
via()
позволяет использовать уже определенное отношение в классе для определения отношения. -
viaTable()
альтернативно позволяет вам определить таблицу для использования в отношении.
Метод via()
позволяет использовать уже определенное отношение, как через таблицу . В нашем примере, однако, таблица ArticlesTags не содержит информации, которая нам viaTable()
поэтому я буду использовать метод viaTable()
.
/** * @return \yii\db\ActiveQuery */ public function getTags() { return $this->hasMany(Tags::className(), ['ID' => 'TagsID']) ->viaTable(Articlestags::tableName(), ['ArticlesID' => 'ID']); }
Теперь, когда мы определили все отношения, мы можем начать использовать модель.
Используя модель
Я быстро создам содержимое базы данных вручную.
$phpTag = new \app\models\Tags();
Следует отметить, что приведенный выше код имеет некоторые предположения, которые я объясню;
- Результат логического выражения
save()
не оценивается. Обычно это неуместно, потому что Yii на самом деле вызоветvalidate()
для класса перед тем, как сохранить его в базе данных.INSERT
базы данных не будет выполнен, если какое-либо из правил проверки не будет выполнено. - Вы можете заметить, что атрибуты ID различных экземпляров используются, пока они не установлены. Это можно сделать безопасно, потому что вызов
save()
INSERT
данные, вернет первичный ключ из базы данных и сделает значение свойства ID допустимым. -
$article->LastEdited
— это значение DateTime в базе данных. Я хочу вставить текущую дату и время, вызвав для этого функцию SQLNOW()
. Вы можете сделать это, используя класс Expression, который позволяет использовать различные выражения SQL с экземплярами ActiveRecord.
Затем вы можете снова извлечь статью из базы данных;
//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; } }
Новые и продвинутые способы использования
Yii ActiveRecord, как я уже говорил, прост. Давайте сделаем это интересным и немного углубимся в новую и измененную функциональность Yii 2.0.
Грязные атрибуты
В Yii 2.0 появилась возможность обнаруживать измененные атрибуты. Для ActiveRecord они называются грязными атрибутами, потому что они требуют обновления базы данных. Эта способность теперь по умолчанию позволяет вам видеть, какие атрибуты изменились в модели, и действовать в соответствии с этим. Когда, например, вы массово назначили все атрибуты из формы POST, вы можете получить только измененные атрибуты:
//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
ActiveRecord, расширенный от Model
, теперь реализует черту \yii\base\Arrayable
с помощью метода toArray()
. Это позволяет быстро конвертировать модель с атрибутами в массив. Это также допускает некоторые приятные дополнения.
Обычно вызов toArray()
вызывает функцию fields()
и преобразует их в массив. Необязательный параметр toArray()
дополнительно вызовет extraFields()
который определяет, какие поля также будут включены.
Эти два метода полей реализованы в BaseActiveRecord
и вы можете реализовать их в своей собственной модели для настройки вывода вызова toArray()
.
В моем примере я хотел бы, чтобы расширенный массив содержал все теги статьи, доступные в виде строки через запятую, в выводе моего массива;
public function extraFields() { return [ 'tags'=>function() { if (isset($this->tags)) { $tags = []; foreach($this->tags as $articletag) { $tags[] = $articletag->Tag; } return implode(', ', $tags); } } ]; }
А затем получить массив всех полей и это дополнительное поле из модели;
//Returns all the attributes and the extra tags field $article->toArray([], ['tags']);
События
Yii 1.1 уже реализовал различные события в CActiveRecord, и они все еще существуют в Yii 2.0. Жизненный цикл ActiveRecord в руководстве по Yii 2.0 очень хорошо показывает, как все эти события запускаются при использовании ActiveRecord. Все события запускаются вокруг обычных действий вашего экземпляра ActiveRecord. Наименование событий совершенно очевидно, поэтому вы должны иметь возможность выяснить, когда они запускаются; afterFind()
, beforeValidate()
, afterValidate()
, beforeSave()
, afterSave()
, beforeDelete()
, afterDelete()
.
В моем примере атрибут LastEdited
— это хороший способ продемонстрировать использование события. Я хочу убедиться, что LastEdited
всегда отражает последний раз, когда статья была отредактирована. Я мог бы установить это на два события; beforeSave()
и beforeValidate()
. Мои правила модели определяют LastEdited
как обязательный атрибут, поэтому нам нужно использовать beforeValidate()
чтобы убедиться, что оно также устанавливается в новых экземплярах модели;
public function beforeValidate($insert) { if (parent::beforeValidate($insert)) { $this->LastEdited = new \yii\db\Expression('NOW()'); return true; } return false; }
Обратите внимание, что со всеми этими событиями вы должны вызывать родительский обработчик событий. Возврат false (или ничего!) Из события before в этих функциях останавливает действие.
Поведение
Поведение можно использовать для улучшения функциональности существующего компонента без изменения его кода. Он также может отвечать на события в компоненте, к которому он был прикреплен. Они ведут себя подобно чертам, введенным в PHP 5.4.
Yii 2.0 поставляется с рядом доступных вариантов поведения;
-
yii\behaviors\AttributeBehavior
позволяет вам указать атрибуты, которые должны быть обновлены в указанном событии. Например, вы можете установить для атрибута значение, основанное на безымянной функции в событииBEFORE_INSERT
. -
yii\behaviors\BlameableBehavior
делает то, что вы ожидаете; винить кого-то Вы можете установить два атрибута;createdByAttribute
иupdatedByAttribute
который будет установлен на текущий идентификатор пользователя при создании или обновлении объекта. -
yii\behaviors\SluggableBehavior
позволяет автоматически создавать URL-слаг на основе одного из атрибутов другого атрибута в модели. -
yii\behaviors\TimestampBehavior
позволит вам автоматически создавать и обновлятьcreatedAtAttribute
времени вcreatedAtAttribute
иupdatedAtAttribute
в вашей модели.
Вы, вероятно, видите, что они имеют некоторые практические применения и в моем примере. Предполагая, что человек, который в настоящее время вошел в приложение, является фактическим автором статьи, я мог бы использовать BlameableBehavior
чтобы сделать их автором, и я также могу использовать TimestampBehaviour
чтобы убедиться, что атрибут LastEdited
остается актуальным. Это заменит мою предыдущую реализацию beforeValidate()
в моей модели. Вот как я прикрепил поведение к своей модели Articles:
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()'), ], ]; }
Я, конечно, предполагаю, что создатель и редактор статьи — это одно и то же лицо. Поскольку у меня нет созданного поля метки времени, я решил не использовать его, установив для createdAtAttribute
значение false
. Я мог бы, конечно, также установить это в 'LastEdited'
.
Транзакционные операции
Последняя особенность, которую я хочу коснуться, — это возможность автоматически форсировать использование транзакций в модели. С применением внешних ключей также появляется возможность сбоя запросов к базе данных из-за этого. Это можно сделать более изящно, заключив их в транзакцию. Yii позволяет вам определять операции, которые должны быть транзакционными, внедряя в вашу модель функцию transactions()
которая указывает, какие операции и в каких сценариях должны быть включены в транзакцию. Обратите внимание, что вы должны вернуть правило для SCENARIO_DEFAULT
если вы хотите, чтобы это было сделано по умолчанию для операций.
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, ]; }
Вывод
Класс Yii ActiveRecord уже сделал обработку ORM очень простой, Yii 2.0 опирается на эту великолепную базу и расширяет ее еще больше. Гибкость огромна благодаря возможности определять различные сценарии использования, прикреплять поведение и использовать события.
Вот некоторые функции ActiveRecord, которые мне показались наиболее полезными с течением времени и которые приветствуются с выходом Yii 2.0. Вы пропустили функцию ActiveRecord или, возможно, чувствуете, что в Yii ActiveRecord отсутствует отличная функция из другой среды? Пожалуйста, дайте нам знать в комментариях!