Модели

Модели являются частью архитектуры MVC (Модель-Вид-Контроллер). Они представляют собой объекты бизнес данных, правил и логики.

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

  • Атрибуты: представляют собой рабочие данные и могут быть доступны как обычные свойства объекта или элементы массива;
  • Метки атрибутов: задают отображение атрибута;
  • Массовое присвоение: поддержка заполнения нескольких атрибутов в один шаг;
  • Правила проверки: обеспечивают ввод данных на основе заявленных правил проверки;
  • Экспорт Данных: разрешает данным модели быть экспортированными в массивы с настройкой форматов.

Класс Model также является базовым классом для многих расширенных моделей, таких как Active Record. Пожалуйста, обратитесь к соответствующей документации для более подробной информации об этих расширенных моделях.

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

Атрибуты

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

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

$model = new \app\models\ContactForm;

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

Также возможно получить доступ к атрибутам как к элементам массива, спасибо поддержке ArrayAccess и Traversable в yii\base\Model:

$model = new \app\models\ContactForm;

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

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

Определение Атрибутов

По умолчанию, если ваш класс модели расширяется напрямую от yii\base\Model, то все не статичные публичные переменные являются атрибутами. Например, у класса модели ContactForm , который находится ниже, четыре атрибута: name, email, subject и body. Модель ContactForm используется для представления входных данных, полученных из HTML формы.

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(). Например,

$model = new \app\models\ContactForm;

// отобразит "Name"
echo $model->getAttributeLabel('name');

По умолчанию, метки атрибутов автоматически генерируются из названия атрибута. Генерация выполняется методом yii\base\Model::generateAttributeLabel(). Он превращает первую букву каждого слова в верхний регистр, если имена переменных состоят из нескольких слов. Например, username станет Username, а firstName станет First Name.

Если Вы не хотите использовать автоматически сгенерированные метки, Вы можете переопределить метод yii\base\Model::attributeLabels(), чтобы явно объявить метку атрибута. Например,

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',
        ];
    }
}

Для приложений поддерживающих мультиязычность, Вы можете перевести метки атрибутов. Это можно сделать в методе attributeLabels() как показано ниже:

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'),
    ];
}

Можно даже условно определять метки атрибутов. Например, на основе сценариев и использованной в нём модели , Вы можете возвращать различные метки для одного и того же атрибута.

Для справки: Строго говоря, метки атрибутов являются частью видов. Но объявление меток в моделях часто очень удобно и приводит к чистоте кода и повторному его использованию.

Сценарии

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

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

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

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

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

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.

Метод scenarios() возвращает массив, ключами которого являются имена сценариев, а значения - соответствующие активные атрибуты. Активные атрибуты могут быть массово присвоены и подлежат валидации. В приведенном выше примере, атрибуты username и password это активные атрибуты сценария login, а в сценарии register так же активным атрибутом является email вместе с username и password.

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

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

Возможности сценариев в основном используются валидацией и массовым присвоением атрибутов. Однако, Вы можете использовать их и для других целей. Например, Вы можете различным образом объявлять метки атрибутов на основе текущего сценария.

Правила валидации

Когда данные модели, получены от конечных пользователей, они должны быть проверены, для того чтобы убедиться, что данные удовлетворяют определенным правилам (так называемым правилам валидации также известными как бизнес-правила). Например, дана модель ContactForm, возможно Вы захотите убедиться, что все атрибуты являются не пустыми значениями, а атрибут email содержит допустимый адрес электронной почты. Если значения нескольких атрибутов не удовлетворяют соответствующим бизнес-правилам, то должны быть показаны соответствующие сообщения об ошибках, чтобы помочь конечному пользователю исправить допущенные ошибки.

Вы можете вызвать yii\base\Model::validate() для проверки полученных данных. Данный метод будет использовать правила валидации определённые в yii\base\Model::rules() для проверки каждого соответствующего атрибута. Если ошибок не найдено, то возвращается true, в противном случае возвращается false, а ошибки содержит свойство yii\base\Model::errors. Например,

$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:

public function rules()
{
    return [
        // name, email, subject и body атрибуты обязательны
        [['name', 'email', 'subject', 'body'], 'required'],

        // атрибут email должен быть правильным email адресом
        ['email', 'email'],
    ];
}

Правило может использоваться для проверки одного или нескольких атрибутов, также и атрибут может быть проверен одним или несколькими правилами. Пожалуйста, обратитесь к разделу Проверка входных значений для более подробной информации о том, как объявлять правила проверки.

Иногда необходимо, чтобы правила применялись только в определенных сценариях. Чтобы это сделать необходимо указать свойство on в правилах, следующим образом:

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.

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

Массовое Присвоение

Массовое присвоение - это удобный способ заполнения модели данными вводимыми пользователем с помощью одной строки кода. Он заполняет атрибуты модели путем присвоения входных данных непосредственно свойству yii\base\Model::$attributes. Следующие два куска кода эквивалентны, они оба пытаются присвоить данные из формы представленные конечными пользователями атрибутам модели ContactForm. Ясно, что первый код гораздо чище и менее подвержен ошибкам, чем второй:

$model = new \app\models\ContactForm;
$model->attributes = \Yii::$app->request->post('ContactForm');
$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 модели. Например, если модель User имеет следующий заданный сценарий, в данном случае это сценарий login, то только username и password могут быть массово присвоены. Любые другие атрибуты останутся нетронутыми.

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 являются безопасными атрибутами.

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

Небезопасные атрибуты

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

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

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

$model->secret = $secret;

Экспорт Данных

Часто нужно экспортировать модели в различные форматы. Например, может потребоваться преобразовать коллекцию моделей в JSON или Excel формат. Процесс экспорта может быть разбит на два самостоятельных шага. На первом этапе модели преобразуются в массивы; на втором этапе массивы преобразуются в целевые форматы. Вы можете сосредоточиться только на первом шаге потому, что второй шаг может быть достигнут путем универсального инструмента форматирования данных, такого как yii\web\JsonResponseFormatter.

Самый простой способ преобразования модели в массив - использовать свойство yii\base\Model::$attributes. Например

$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.

Поля

Поле - это просто именованный элемент в массиве, который может быть получен вызовом метода yii\base\Model::toArray() модели.

По умолчанию имена полей эквивалентны именам атрибутов. Однако, это поведение можно изменить, переопределив методы fields() и/или extraFields(). Оба метода должны возвращать список определенных полей. Поля определённые fields() являются полями по умолчанию, это означает, что toArray() будет возвращать эти поля по умолчанию. Метод extraFields() определяет дополнительно доступные поля, которые также могут быть возвращены toArray() так много, как Вы укажите их через параметр $expand. Например, следующий код будет возвращать все поля определённые в fields(), а также поля prettyName и fullAddress, если они определены в extraFields().

$array = $model->toArray([], ['prettyName', 'fullAddress']);

Вы можете переопределить fields() чтобы добавить, удалить, переименовать или переопределить поля. Возвращаемым значением fields() должен быть массив. Ключами массива являются имена полей, а значениями - соответствующие определения полей, которые могут быть либо именами свойств/атрибутов, либо анонимными функциями, возвращающими соответствующие значения полей. В частном случае, когда имя поля совпадает с именем его атрибута, возможно опустить ключ массива. Например,

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

Лучшие практические методики разработки моделей

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

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

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

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

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

Например, в шаблоне приложения advanced, Вы можете определить базовым классом модели common\models\Post. Тогда для frontend приложения, Вы определяете и используете конкретный класс модели frontend\models\Post, который расширяется от common\models\Post. И аналогичным образом для backend приложения, Вы определяете backend\models\Post. С помощью такой стратегии, можно быть уверенным, что код в frontend\models\Post используется только для конкретного frontend приложения, и если делаются любые изменения в нём, то не нужно беспокоиться, что изменения могут сломать backend приложение.