Модели ====== Модели являются частью архитектуры [MVC](http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) (Модель-Вид-Контроллер). Они представляют собой объекты бизнес данных, правил и логики. Вы можете создавать классы моделей путём расширения класса [[yii\base\Model]] или его дочерних классов. Базовый класс [[yii\base\Model]] поддерживает много полезных функций: * [Атрибуты](#attributes): представляют собой рабочие данные и могут быть доступны как обычные свойства объекта или элементы массыва; * [Метки атрибутов](#attribute-labels): задают отображение атрибута; * [Массовое присвоение](#massive-assignment): поддержка заполнения нескольких атрибутов в один шаг; * [Правила проверки](#validation-rules): обеспечивают ввод данных на основе заявленных правил проверки; * [Экспорт Данных](#data-exporting): разрешает данным модели быть экспортированными в массивы с настройкой форматов. Класс `Model` также является базовым классом для многих расширенных моделей, таких как [Active Record](db-active-record.md). Пожалуйста, обратитесь к соответствующей документации для более подробной информации об этих расширенных моделях. > Для справки: Вы не обязаны основывать свои классы моделей на [[yii\base\Model]]. Однако, поскольку в yii есть много компонентов, созданных для поддержки [[yii\base\Model]], обычно так делать предпочтительнее для базового класса модели. ## Атрибуты Модели предоставляют рабочие данные в терминах *атрибутах*. Каждый атрибут представляет собой публично доступное свойство модели. Метод [[yii\base\Model::attributes()]] определяет какие атрибуты имеет класс модели. Вы можете получить доступ к атрибуту как к обычному свойству объекта: ```php $model = new \app\models\ContactForm; // "name" - это атрибут модели ContactForm $model->name = 'example'; echo $model->name; ``` Также возможно получить доступ к атрибутам как к элементам массива, спасибо поддержке [ArrayAccess](http://php.net/manual/en/class.arrayaccess.php) и [ArrayIterator](http://php.net/manual/en/class.arrayiterator.php) в [[yii\base\Model]]: ```php $model = new \app\models\ContactForm; // доступ к атрибутам как к элементам массива $model['name'] = 'example'; echo $model['name']; // перебор атрибутов foreach ($model as $name => $value) { echo "$name: $value\n"; } ``` ### Определение Атрибутов По умолчанию, если ваш класс модели расширяется напрямую от [[yii\base\Model]], то все *не статичные публичные* переменные являются атрибутами. Например, у класса модели `ContactForm` , который находится ниже, четыре атрибута: `name`, `email`, `subject` и `body`. Модель `ContactForm` используется для представления входных данных, полученных из HTML формы. ```php namespace app\models; use yii\base\Model; class ContactForm extends Model { public $name; public $email; public $subject; public $body; } ``` Вы можете переопределить метод [[yii\base\Model::attributes()]], чтобы определять атрибуты другим способом. Метод должен возвращать имена атрибутов в модели. Например [[yii\db\ActiveRecord]] делает так, возвращая имена столбцов из связанной таблицы базы данных в качестве имён атрибутов. Также может понадобиться переопределить магические методы, такие как `__get()`, `__set()` для того, что бы атрибуты могли быть доступны как обычные свойства объекта. ### Метки атрибутов При отображении значений или при получении ввода значений атрибутов, часто требуется отобразить некоторые надписи, связанные с атрибутами. Например, если атрибут назван `firstName`, Вы можете отобразить его как `First Name`, что является более удобным для пользователя, в тех случаях, когда атрибут отображается конечным пользователям в таких местах, как форма входа и сообщения об ошибках. Вы можете получить метку атрибута, вызвав [[yii\base\Model::getAttributeLabel()]]. Например, ```php $model = new \app\models\ContactForm; // отобразит "Name" echo $model->getAttributeLabel('name'); ``` По умолчанию, метки атрибутов автоматически генерируются из названия атрибута. Генерация выполняется методом [[yii\base\Model::generateAttributeLabel()]]. Он превращает первую букву каждого слова в верхний регистр, если имена переменных состоят из нескольких слов. Например, `username` станет `Username`, а `firstName` станет `First Name`. Если Вы не хотите использовать автоматически сгенерированные метки, Вы можете переопределить метод [[yii\base\Model::attributeLabels()]], чтобы явно объявить метку атрибута. Например, ```php namespace app\models; use yii\base\Model; class ContactForm extends Model { public $name; public $email; public $subject; public $body; public function attributeLabels() { return [ 'name' => 'Your name', 'email' => 'Your email address', 'subject' => 'Subject', 'body' => 'Content', ]; } } ``` Для приложений поддерживающих мультиязычность, Вы можете перевести метки атрибутов. Это можно сделать в методе [[yii\base\Model::attributeLabels()|attributeLabels()]] как показано ниже: ```php public function attributeLabels() { return [ 'name' => \Yii::t('app', 'Your name'), 'email' => \Yii::t('app', 'Your email address'), 'subject' => \Yii::t('app', 'Subject'), 'body' => \Yii::t('app', 'Content'), ]; } ``` Можно даже условно определять метки атрибутов. Например, на основе [сценариев](#scenarios) и использованной в нём модели , Вы можете возвращать различные метки для одного и того же атрибута. > Для справки: Строго говоря, метки атрибутов являются частью [видов](structure-views.md). Но объявление меток в моделях часто очень удобно и приводит к чистоте кода и повторному его использованию. ## Сценарии Модель может быть использованна в различных *сценариях*. Например, модель `User` может быть использованна для коллекции входных логинов пользователей, а также может быть использованна для цели регистрации пользователей. В различных сценариях, модель может использовать различные бизнес-правила и логику. Например, атрибут `email` может потребоваться во время регистрации пользователя, но не во время входа пользователя в систему. Модель использует свойство [[yii\base\Model::scenario]], чтобы отслеживать сценарий, в котором она используется. По умолчанию, модель поддерживает только один сценарий с именем `default`. В следующем коде показано два способа установки сценария модели: ```php // сценарий задается как свойство $model = new User; $model->scenario = 'login'; // сценарий задается через конфигурацию $model = new User(['scenario' => 'login']); ``` По умолчанию сценарии, поддерживаемые моделью, определяются [правилами валидации](#validation-rules) объявленными в модели. Однако, Вы можете изменить это поведение путем переопределения метода [[yii\base\Model::scenarios()]] как показано ниже: ```php namespace app\models; use yii\db\ActiveRecord; class User extends ActiveRecord { public function scenarios() { return [ 'login' => ['username', 'password'], 'register' => ['username', 'email', 'password'], ]; } } ``` > Для справки: В приведенном выше и следующих примерах, классы моделей расширяются от [[yii\db\ActiveRecord]] потому, что использование нескольких сценариев обычно происходит от классов [Active Record](db-active-record.md). Метод `scenarios()` возвращает массив, ключами которого являются имена сценариев, а значения - соответствующие *активные атрибуты*. Активные атрибуты могут быть [массово присвоены](#massive-assignment) и подлежат [валидации](#validation-rules). В приведенном выше примере, атрибуты `username` и `password` это активные атрибуты сценария `login`, а в сценарии `register` так же активным атрибутом является `email` вместе с `username` и `password`. По умолчанию реализация `scenarios()` вернёт все найденные сценарии в правилах валидации задекларированных в методе [[yii\base\Model::rules()]]. При переопределении метода `scenarios()`, если Вы хотите ввести новые сценарии помимо стандартных, Вы можете написать код на основе следующего примера: ```php namespace app\models; use yii\db\ActiveRecord; class User extends ActiveRecord { public function scenarios() { $scenarios = parent::scenarios(); $scenarios['login'] = ['username', 'password']; $scenarios['register'] = ['username', 'email', 'password']; return $scenarios; } } ``` Возможности сценариев в основном используются [валидацией](#validation-rules) и [массовым присвоением атрибутов](#massive-assignment). Однако, Вы можете использовать их и для других целей. Например, Вы можете различным образом объявлять [метки атрибутов](#attribute-labels) на основе текущего сценария. ## Правила валидации Когда данные модели, получены от конечных пользователей, они должны быть проверены, для того чтобы убедиться, что данные удовлетворяют определенным правилам (так называемым *правилам валидации* также известными как *бизнес-правила*). Например, дана модель `ContactForm`, возможно Вы захотите убедиться, что все атрибуты являются не пустыми значениями, а атрибут `email` содержит допустимый адрес электронной почты. Если значения нескольких атрибутов не удовлетворяют соответствующим бизнес-правилам, то должны быть показаны соответствующие сообщения об ошибках, чтобы помочь конечному пользователю исправить допущенные ошибки. Вы можете вызвать [[yii\base\Model::validate()]] для проверки полученных данных. Данный метод будет использовать правила валидации определённые в [[yii\base\Model::rules()]] для проверки каждого соответствующего атрибута. Если ошибок не найдено, то возвращается True, в противном случае возвращается false, а ошибки содержит свойство [[yii\base\Model::errors]]. Например, ```php $model = new \app\models\ContactForm; // модель заполнения атрибутов данными, вводимыми пользователем $model->attributes = \Yii::$app->request->post('ContactForm'); if ($model->validate()) { // все данные верны } else { // проверка не удалась: $errors - это массив содержащий сообщения об ошибках $errors = $model->errors; } ``` Объявляем правила валидации связанные с моделью, переопределяем метод [[yii\base\Model::rules()]] возврата правил атрибутов модели которые следует удовлетворить. В следующем примере показаны правила проверки объявленные в модели `ContactForm`: ```php public function rules() { return [ // name, email, subject и body атрибуты обязательны [['name', 'email', 'subject', 'body'], 'required'], // атрибут email должен быть правильным email адресом ['email', 'email'], ]; } ``` Правило может использоваться для проверки одного или нескольких атрибутов, также и атрибут может быть проверен одним или несколькими правилами. Пожалуйста, обратитесь к разделу [Проверка входных значений](input-validation.md) для более подробной информации о том, как объявлять правила проверки. Иногда необходимо, чтобы правила применялись только в определенных [сценариях](#scenarios). Чтобы это сделать необходимо указать свойство `on` в правилах, следующим образом: ```php public function rules() { return [ // username, email и password требуются в сценарии "register" [['username', 'email', 'password'], 'required', 'on' => 'register'], // username и password требуются в сценарии "login" [['username', 'password'], 'required', 'on' => 'login'], ]; } ``` Если не указать свойство `on`, то правило применяется во всех сценариях. Правило называется *активным правилом* если оно может быть применено в текущем сценарии [[yii\base\Model::scenario|scenario]]. Атрибут будет проверяться тогда и только тогда если он является активным атрибутом объявленным в `scenarios()` и связаным с одним или несколькими активными правилами, объявленными в `rules()`. ## Массовое Присвоение Массовое присвоение - это удобный способ заполнения модели данными вводимыми пользователем с помощью одной строки кода. Он заполняет атрибуты модели путем присвоения входных данных непосредственно свойству [[yii\base\Model::$attributes]]. Следующие два куска кода эквивалентны, они оба пытаются присвоить данные из формы представленные конечными пользователями атрибутам модели `ContactForm`. Ясно, что первый код гораздо чище и менее подвержен ошибкам, чем второй: ```php $model = new \app\models\ContactForm; $model->attributes = \Yii::$app->request->post('ContactForm'); ``` ```php $model = new \app\models\ContactForm; $data = \Yii::$app->request->post('ContactForm', []); $model->name = isset($data['name']) ? $data['name'] : null; $model->email = isset($data['email']) ? $data['email'] : null; $model->subject = isset($data['subject']) ? $data['subject'] : null; $model->body = isset($data['body']) ? $data['body'] : null; ``` ### Безопасные Атрибуты Массовое присвоение применяется только к так называемым *безопасным атрибутам*, которые являются атрибутами, перечисленными в [[yii\base\Model::scenarios()]] в текущем сценарии [[yii\base\Model::scenario|scenario]] модели. Например, если модель `User` имеет следующий заданный сценарий, в данном случае это сценарий `login`, то только `username` и `password` могут быть массово присвоены. Любые другие атрибуты остануться нетронутыми. ```php public function scenarios() { return [ 'login' => ['username', 'password'], 'register' => ['username', 'email', 'password'], ]; } ``` > Для справки: Причиной того, что массовое присвоение атрибутов применяется только к безопасным атрибутам, является то, что необходимо контролировать какие атрибуты могут быть изменены конечными пользователями. Например, если модель `User` имеет атрибут `permission`, который определяет разрешения, назначенные пользователю, то необходимо быть уверенным, что данный атрибут может быть изменён только администраторами через бэкэнд-интерфейс. По умолчанию реализация [[yii\base\Model::scenarios()]] будет возвращать все сценарии и атрибуты найденные в [[yii\base\Model::rules()]], если не переопределить этот метод, это будет означать, что атрибуты являются безопасными до тех пор пока они не появятся в одном из активных правил проверки. По этой причине существует специальный валидатор с псевдонимом `safe`, он предоставляет возможность объявить атрибут безопасным без фактической его проверки. Например, следующие правила определяют, что оба атрибута `title` и `description` являются безопасными атрибутами. ```php public function rules() { return [ [['title', 'description'], 'safe'], ]; } ``` ### Небезопасные атрибуты Как сказано выше, метод [[yii\base\Model::scenarios()]] служит двум целям: определения, какие атрибуты должны быть проверены, и определения, какие атрибуты являются безопасными (т.е. не требуют проверки). В некоторых случаях необходимо проверить атрибут не объявляя его безопасным. Вы можете сделать это с помощью префикса восклицательный знак `!` в имени атрибута при объявлении его в `scenarios()` как атрибут `secret` в следующем примере: ```php public function scenarios() { return [ 'login' => ['username', 'password', '!secret'], ]; } ``` Когда модель будет присутствовать в сценарии `login`, то все три эти атрибута будут проверены. Однако, только атрибуты `username` и `password` могут быть массово присвоены. Назначить входное значение атрибуту `secret` нужно явно следующим образом, ```php $model->secret = $secret; ``` ## Экспорт Данных Часто нужно экспортировать модели в различные форматы. Например, может потребоваться преобразовать коллекцию моделей в JSON или Excel формат. Процесс экспорта может быть разбит на два самостоятельных шага. На первом этапе модели преобразуются в массивы; на втором этапе массивы преобразуются в целевые форматы. Вы можете сосредоточиться только на первом шаге потому, что второй шаг может быть достигнут путем универсального инструмента форматирования данных, такого как [[yii\web\JsonResponseFormatter]]. Самый простой способ преобразования модели в массив - использовать свойство [[yii\base\Model::$attributes]]. Например, ```php $post = \app\models\Post::findOne(100); $array = $post->attributes; ``` По умолчанию, свойство [[yii\base\Model::$attributes]] возвращает значения *всех* атрибутов объявленных в [[yii\base\Model::attributes()]]. Более гибкий и мощный способ конвертирования модели в массив - использовать метод [[yii\base\Model::toArray()]]. Его поведение по умолчанию такое же как и у [[yii\base\Model::$attributes]]. Тем не менее, он позволяет выбрать, какие элементы данных, называемые *полями*, поставить в результирующий массив и как они должны быть отформатированы. На самом деле, этот способ экспорта моделей по умолчанию применяется при разработке в RESTful Web service, как описано в [Response Formatting](rest-response-formatting.md). ### Поля Поле - это просто именованный элемент в массиве, который может быть получен вызовом метода [[yii\base\Model::toArray()]] модели. По умолчанию имена полей эквивалентны именам атрибутов. Однако, это поведение можно изменить, переопределив методы [[yii\base\Model::fields()|fields()]] и/или [[yii\base\Model::extraFields()|extraFields()]]. Оба метода должны возвращать список определенных полей. Поля определённые `fields()` являются полями по умолчанию, это означает, что `toArray()` будет возвращать эти поля по умолчанию. Метод `extraFields()` определяет дополнительно доступные поля, которые также могут быть возвращены `toArray()` так много, как Вы укажите их через параметр `$expand`. Например, следующий код будет возвращать все поля определённые в `fields()`, а также поля `prettyName` и `fullAddress`, если они определены в `extraFields()`. ```php $array = $model->toArray([], ['prettyName', 'fullAddress']); ``` Вы можете переопределить `fields()` чтобы добавить, удалить, переименовать или переопределить поля. Возвращаемым значением `fields()` должен быть массив. Ключами массива являются имена полей, а значениями - соответствующие определения полей, которые могут быть либо именами свойств/атрибутов, либо анонимными функциями, возвращающими соответствующие значения полей. В частном случае, когда имя поля совпадает с именем его атрибута, возможно опустить ключ массива. Например, ```php // использовать явное перечисление всех полей, лучше всего тогда, когда вы хотите убедиться, // что изменения в вашей таблице базы данных или атрибуте модели не вызывают изменение вашего поля // (для поддержания обратной совместимости API интерфейса). public function fields() { return [ // здесь имя поля совпадает с именем атрибута 'id', // здесь имя поля - "email", соответствующее ему имя атрибута - "email_address" 'email' => 'email_address', // здесь имя поля - "name", а значение определяется обратным вызовом PHP 'name' => function () { return $this->first_name . ' ' . $this->last_name; }, ]; } // использовать фильтрование нескольких полей, лучше тогда, когда вы хотите наследовать // родительскую реализацию и черный список некоторых "чувствительных" полей. public function fields() { $fields = parent::fields(); // удаляем поля, содержащие конфиденциальную информацию unset($fields['auth_key'], $fields['password_hash'], $fields['password_reset_token']); return $fields; } ``` > Внимание: по умолчанию все атрибуты модели будут включены в экспортируемый массив, вы должны проверить ваши данные и убедиться, что они не содержат конфиденциальной информации. Если такая информация присутствует, вы должны переопределить `fields()` и отфильтровать поля. В приведенном выше примере мы выбираем и отфильтровываем `auth_key`, `password_hash` и `password_reset_token`. ## Лучшие практические методики разработки моделей Модели являются центральным местом представления бизнес-данных, правил и логики. Они часто повторно используются в разных местах. В хорошо спроектированном приложении, модели, как правило, намного больше, чем [контроллеры](structure-controllers.md). В целом, модели * могут содержать атрибуты для представления бизнес-данных; * могут содержать правила проверки для обеспечения целостности и достоверности данных; * могут содержать методы с реализацией бизнес-логики; * не следует напрямую задавать запрос на доступ, либо сессии, либо любые другие данные об окружающей среде. Эти данные должны быть введены [контроллерами](structure-controllers.md) в модели; * следует избегать встраивания HTML или другого отображаемого кода - это лучше делать в [видах](structure-views.md); * избегайте слишком большого количества [сценариев](#scenarios) в одной модели. Рекомендации выше обычно учитываются при разработке больших сложных систем. В таких системах, модели могут быть очень большими, в связи стем, что они используются во многих местах и поэтому могут содержать множество наборов правил и бизнес-логики. Это часто заканчивается кошмаром при поддержании кода модели, поскольку одним касанием кода можно повлиять на несколько разных мест. Чтобы сделать код модели более легким в обслуживании, Вы можете предпринять следующую стратегию: * Определить набор базовых классов моделей, которые являются общими для разных [приложений](structure-applications.md) или [модулей](structure-modules.md). Эти классы моделей должны содержать минимальный набор правил и логики, которые являются общими среди всех используемых приложений или модулей. * В каждом [приложении](structure-applications.md) или [модуле](structure-modules.md) в котором используется модель, определить конкретный класс модели (или классы моделей), отходящий от соответствующего базового класса модели. Конкретный класс модели должен содержать правила и логику, которые являются специфическими для данного приложения или модуля. Например, в [Дополнительном Шаблоне Проекта](https://github.com/yiisoft/yii2-app-advanced/blob/master/docs/guide/README.md), Вы можете определить базовым классом модели `common\models\Post`. Тогда для frontend приложения, Вы определяете и используете конкретный класс модели `frontend\models\Post`, который расширяется от `common\models\Post`. И аналогичным образом для backend приложения, Вы определяете `backend\models\Post`. С помощью такой стратегии, можно быть уверенным, что код в `frontend\models\Post` используется только для конкретного frontend приложения, и если делаются любые изменения в нём, то не нужно беспокоиться, что изменения могут сломать backend приложение.