Модели
======

Модели являются частью архитектуры [MVC](https://ru.wikipedia.org/wiki/Model-View-Controller) (Модель-Вид-Контроллер). Они представляют собой объекты бизнес данных, правил и логики.

Вы можете создавать классы моделей путём расширения класса [[yii\base\Model]] или его дочерних классов. Базовый класс [[yii\base\Model]] поддерживает много полезных функций:

* [Атрибуты](#attributes): представляют собой рабочие данные и могут быть доступны как обычные свойства объекта или элементы массива;
* [Метки атрибутов](#attribute-labels): задают отображение атрибута;
* [Массовое присвоение](#massive-assignment): поддержка заполнения нескольких атрибутов в один шаг;
* [Правила проверки](#validation-rules): обеспечивают ввод данных на основе заявленных правил проверки;
* [Экспорт Данных](#data-exporting): разрешает данным модели быть экспортированными в массивы с настройкой форматов.

Класс `Model` также является базовым классом для многих расширенных моделей, таких как [Active Record](db-active-record.md). Пожалуйста, обратитесь к соответствующей документации для более подробной информации об этих расширенных моделях.

> Info: Вы не обязаны основывать свои классы моделей на [[yii\base\Model]]. Однако, поскольку в yii есть много компонентов, созданных для поддержки [[yii\base\Model]], обычно так делать предпочтительнее для базового класса модели.

## Атрибуты <span id="attributes"></span>

Модели предоставляют рабочие данные в терминах *атрибутах*. Каждый атрибут представляет собой публично доступное свойство модели. Метод [[yii\base\Model::attributes()]] определяет какие атрибуты имеет класс модели.

Вы можете получить доступ к атрибуту как к обычному свойству объекта:

```php
$model = new \app\models\ContactForm;

// "name" - это атрибут модели ContactForm
$model->name = 'example';
echo $model->name;
```

Также возможно получить доступ к атрибутам как к элементам массива, спасибо поддержке [ArrayAccess](https://www.php.net/manual/ru/class.arrayaccess.php) и [Traversable](https://www.php.net/manual/ru/class.traversable.php)
в [[yii\base\Model]]:

```php
$model = new \app\models\ContactForm;

// доступ к атрибутам как к элементам массива
$model['name'] = 'example';
echo $model['name'];

// Модель является обходимой(traversable) с использованием foreach.
foreach ($model as $name => $value) {
    echo "$name: $value\n";
}
```


### Определение Атрибутов <span id="defining-attributes"></span>

По умолчанию, если ваш класс модели расширяется напрямую от [[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()` для того, чтобы атрибуты могли быть доступны как обычные свойства объекта.


### Метки атрибутов <span id="attribute-labels"></span>

При отображении значений или при получении ввода значений атрибутов, часто требуется отобразить некоторые надписи, связанные с атрибутами. Например, если атрибут назван `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). Но объявление меток в моделях часто очень удобно и приводит к чистоте кода и повторному его использованию.

## Сценарии <span id="scenarios"></span>

Модель может быть использована в различных *сценариях*. Например, модель `User` может быть использована для коллекции входных логинов пользователей, а также может быть использована для цели регистрации пользователей.  	
В различных сценариях, модель может использовать различные бизнес-правила и логику. Например, атрибут `email` может потребоваться во время регистрации пользователя, но не во время входа пользователя в систему.

Модель использует свойство [[yii\base\Model::scenario]], чтобы отслеживать сценарий, в котором она используется. По умолчанию, модель поддерживает только один сценарий с именем `default`. В следующем коде показано два способа установки сценария модели:

```php
// сценарий задается как свойство
$model = new User;
$model->scenario = User::SCENARIO_LOGIN;

// сценарий задается через конфигурацию
$model = new User(['scenario' => User::SCENARIO_LOGIN]);
```

По умолчанию сценарии, поддерживаемые моделью, определяются [правилами валидации](#validation-rules) объявленными
в модели. Однако Вы можете изменить это поведение путем переопределения метода [[yii\base\Model::scenarios()]] как показано ниже:

```php
namespace app\models;

use yii\db\ActiveRecord;

class User extends ActiveRecord
{
    const SCENARIO_LOGIN = 'login';
    const SCENARIO_REGISTER = 'register';

    public function scenarios()
    {
        return [
            self::SCENARIO_LOGIN => ['username', 'password'],
            self::SCENARIO_REGISTER => ['username', 'email', 'password'],
        ];
    }
}
```

> Info: В приведенном выше и следующих примерах, классы моделей расширяются от [[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
{
    const SCENARIO_LOGIN = 'login';
    const SCENARIO_REGISTER = 'register';

    public function scenarios()
    {
        $scenarios = parent::scenarios();
        $scenarios[self::SCENARIO_LOGIN] = ['username', 'password'];
        $scenarios[self::SCENARIO_REGISTER] = ['username', 'email', 'password'];
        return $scenarios;
    }
}
```

Возможности сценариев в основном используются [валидацией](#validation-rules) и [массовым присвоением атрибутов](#massive-assignment). Однако, Вы можете использовать их и для других целей. Например, Вы можете различным образом объявлять [метки атрибутов](#attribute-labels) на основе текущего сценария.


##  Правила валидации <span id="validation-rules"></span>

Когда данные модели, получены от конечных пользователей, они должны быть проверены, для того чтобы убедиться, что данные удовлетворяют определенным правилам (так называемым *правилам валидации* также известными как *бизнес-правила*). Например, дана модель `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' => self::SCENARIO_REGISTER],

        // username и password требуются в сценарии "login"
        [['username', 'password'], 'required', 'on' => self::SCENARIO_LOGIN],
    ];
}
```

Если не указать свойство `on`, то правило применяется во всех сценариях. Правило называется *активным правилом* если оно может быть применено в текущем сценарии [[yii\base\Model::scenario|scenario]].

Атрибут будет проверяться тогда и только тогда если он является активным атрибутом объявленным в `scenarios()` и
связанным с одним или несколькими активными правилами, объявленными в `rules()`.

## Массовое Присвоение <span id="massive-assignment"></span>

Массовое присвоение - это удобный способ заполнения модели данными вводимыми пользователем с помощью одной строки кода. Он заполняет атрибуты модели путем присвоения входных данных непосредственно свойству [[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;
```


### Безопасные Атрибуты <span id="safe-attributes"></span>

Массовое присвоение применяется только к так называемым *безопасным атрибутам*, которые являются атрибутами, перечисленными в [[yii\base\Model::scenarios()]] в текущем сценарии [[yii\base\Model::scenario|scenario]] модели. Например, если модель `User` имеет следующий заданный сценарий, в данном случае это сценарий `login`, то только `username` и `password` могут быть массово присвоены. Любые другие атрибуты останутся нетронутыми.

```php
public function scenarios()
{
    return [
        self::SCENARIO_LOGIN => ['username', 'password'],
        self::SCENARIO_REGISTER => ['username', 'email', 'password'],
    ];
}
```

> Info: Причиной того, что массовое присвоение атрибутов применяется только к безопасным атрибутам, является то, что необходимо контролировать какие атрибуты могут быть изменены конечными пользователями. Например, если модель `User` имеет атрибут `permission`, который определяет разрешения, назначенные пользователю, то необходимо быть уверенным, что данный атрибут может быть изменён только администраторами через бэкэнд-интерфейс.

По умолчанию [[yii\base\Model::scenarios()]] будет возвращать все сценарии и атрибуты найденные в [[yii\base\Model::rules()]], если не переопределить этот метод, атрибут будет считаться безопасным только в случае, если он участвует в любом из активных правил проверки.

По этой причине существует специальный валидатор с псевдонимом `safe`, он предоставляет возможность объявить атрибут безопасным без фактической его проверки. Например, следующие правила определяют, что оба атрибута `title` и `description` являются безопасными атрибутами.

```php
public function rules()
{
    return [
        [['title', 'description'], 'safe'],
    ];
}
```


### Небезопасные атрибуты <span id="unsafe-attributes"></span>

Как сказано выше, метод [[yii\base\Model::scenarios()]] служит двум целям: определения, какие атрибуты должны быть проверены, и определения, какие атрибуты являются безопасными (т.е. не требуют проверки). В некоторых случаях необходимо проверить атрибут не объявляя его безопасным. Вы можете сделать это с помощью префикса восклицательный знак `!` в имени атрибута при объявлении его в `scenarios()` как атрибут `secret` в следующем примере:

```php
public function scenarios()
{
    return [
        self::SCENARIO_LOGIN => ['username', 'password', '!secret'],
    ];
}
```

Когда модель будет присутствовать в сценарии `login`, то все три эти атрибута будут проверены. Однако, только атрибуты `username` и `password` могут быть массово присвоены. Назначить входное значение атрибуту `secret` нужно явно следующим образом,

```php
$model->secret = $secret;
```


## Экспорт Данных <span id="data-exporting"></span>

Часто нужно экспортировать модели в различные форматы. Например, может потребоваться преобразовать коллекцию моделей в 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).

### Поля <span id="fields"></span>

Поле - это просто именованный элемент в массиве, который может быть получен вызовом метода [[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;
}
```

> Warning: по умолчанию все атрибуты модели будут включены в экспортируемый массив, вы должны проверить ваши данные и убедиться, что они не содержат конфиденциальной информации. Если такая информация присутствует, вы должны переопределить `fields()` и отфильтровать поля. В приведенном выше примере мы выбираем и отфильтровываем `auth_key`, `password_hash` и `password_reset_token`.

## Лучшие практические методики разработки моделей <span id="best-practices"></span>

Модели являются центральным местом представления бизнес-данных, правил и логики. Они часто повторно используются в разных местах. В хорошо спроектированном приложении, модели, как правило, намного больше, чем [контроллеры](structure-controllers.md).

В целом, модели

* могут содержать атрибуты для представления бизнес-данных;
* могут содержать правила проверки для обеспечения целостности и достоверности данных;
* могут содержать методы с реализацией бизнес-логики;
* не следует напрямую задавать запрос на доступ, либо сессии, либо любые другие данные об окружающей среде. Эти данные должны быть введены [контроллерами](structure-controllers.md) в модели;
* следует избегать встраивания HTML или другого отображаемого кода - это лучше делать в [видах](structure-views.md);
* избегайте слишком большого количества [сценариев](#scenarios) в одной модели.

Рекомендации выше обычно учитываются при разработке больших сложных систем. В таких системах, модели могут быть очень большими, в связи с тем, что они используются во многих местах и поэтому могут содержать множество наборов правил и бизнес-логики. Это часто заканчивается кошмаром при поддержании кода модели, поскольку одним касанием кода можно повлиять на несколько разных мест. Чтобы сделать код модели более легким в обслуживании, Вы можете предпринять следующую стратегию:

* Определить набор базовых классов моделей, которые являются общими для разных [приложений](structure-applications.md) или [модулей](structure-modules.md). Эти классы моделей должны содержать минимальный набор правил и логики, которые являются общими среди всех используемых приложений или модулей.
* В каждом [приложении](structure-applications.md) или [модуле](structure-modules.md) в котором используется модель, определить конкретный класс модели (или классы моделей), отходящий от соответствующего базового класса модели. Конкретный класс модели должен содержать правила и логику, которые являются специфическими для данного приложения или модуля.

Например, в [шаблоне приложения advanced](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 приложение.
