Yii2: Как установить значения атрибутов по умолчанию в ActiveRecord?

Это может показаться тривиальным вопросом, однако все очевидные решения, которые я могу придумать, имеют свои недостатки.

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

Значения по умолчанию должны быть установлены и готовы, как только мы создадим экземпляр класса, чтобы (new MyModel)->attr возвращало значение по умолчанию attr.

Вот некоторые из возможностей и проблем, которые у них есть:

  • A) В MyModel переопределите метод init() и назначьте значение по умолчанию, когда isNewRecord истинно, например:

    public function init() {
        if ($this->isNewRecord) {
            $this->attr = 'defaultValue';
        }
        parent::init();
    }
    

    Проблема: поиск. Если мы явно не отключим наш атрибут по умолчанию в MySearchModel (очень подверженный ошибкам, потому что его слишком легко забыть), это также установит значение перед вызовом search() в производном классе MySearchModel и помешает поиску (атрибут attr уже будет установлен, поэтому поиск будет возвращать неверные результаты). В Yii1.1 это было решено вызовом unsetAttributes() перед вызовом search() , однако такого метода нет в Yii2.

  • B) В MyModel переопределите beforeSave() следующим образом:

    public function beforeSave($insert) {
        if ($insert) {
            $this->attr = 'defaultValue';
        }
        return parent::beforeSave();
    }
    

    Проблема: атрибут не установлен в несохраненных записях. (new MyModel)->attr это null. Что еще хуже, даже другие правила проверки, которые полагаются на это значение, не смогут получить к нему доступ, потому что beforeSave() вызывается после проверки.

  • C) Чтобы убедиться, что значение доступно во время проверки, мы можем вместо этого переопределить beforeValidate() и установите там значения по умолчанию следующим образом:

    public function beforeValidate() {
        if ($this->isNewRecord) {
            $this->attr = 'defaultValue';
        }
        return parent::beforeValidate();
    }
    

    Проблема: атрибут по-прежнему не установлен в несохраненных (непроверенных) записях. Нам нужно как минимум вызвать $model->validate(), если мы хотим получить значение по умолчанию.

  • D) Используйте DefaultValidator в rules() чтобы установить значение атрибута по умолчанию во время проверки следующим образом:

    public function rules() {
        return [
            [
                'attr', 'default',
                'value' => 'defaultValue',
                'on' => 'insert', // instantiate model with this scenario
            ],
            // ...
        ];
    }
    

    Проблема: То же, что и B) и C). Значение не устанавливается до тех пор, пока мы не сохраним или не проверим запись.

Так как же является правильным способом установить значения атрибутов по умолчанию? Есть ли другой способ без очерченных проблем?


person mae    schedule 04.09.2016    source источник
comment
$model = новая таблица базы данных(); это все, что вам нужно, $model теперь имеет все атрибуты по умолчанию из таблицы базы данных   -  person Coz    schedule 18.01.2017
comment
@Coz Нет, это не сработает для каких-либо динамических атрибутов, таких как идентификатор текущего пользователя.   -  person mae    schedule 18.01.2017
comment
Да, это будет, просто используйте пользовательский обратный вызов. Тем не менее, если вам нужен идентификатор текущего пользователя, используйте yiiframework.com/ doc-2.0/yii-behaviors-blameablebehavior.html.   -  person Coz    schedule 18.01.2017
comment
Как вы, вероятно, выяснили сами, поведение, включая то, которое вы связали, устанавливает значение по умолчанию только перед вставкой или обновлением записи. Это неправильный ответ, так как нам нужно значение гораздо раньше - при инициализации (см. вариант B).   -  person mae    schedule 19.01.2017
comment
В описании DefaultValidator сказано, что это не настоящий валидатор. Поэтому, если вы создаете правило для значений по умолчанию, вы также должны удалить эти поля из правила required! После этого модель будет содержать значения по умолчанию перед вызовами проверки/сохранения.   -  person Pavel Sokolov    schedule 11.05.2019


Ответы (7)


Это зависание с раздутым многоцелевым ActiveRecords Yii.

По моему скромному мнению, модели форм, активные записи и модели поиска лучше разделить на отдельные классы/подклассы.

Почему бы не разделить модели поиска и модели форм?

abstract class Creature extends ActiveRecord {
    ...
}

class CreatureForm extends Creature {

    public function init() {
        parent::init();
        if ($this->isNewRecord) {
            $this->number_of_legs = 4;
        }
    }
}

class CreatureSearch extends Creature {

    public function search() {
        ...
    }
}

Преимущества этого подхода

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

На самом деле, в нашем последнем проекте мы используем модели поиска, которые вообще не выходят за рамки связанного ActiveRecord.

person Arth    schedule 13.03.2019
comment
Это старый вопрос. С тех пор я почти так и делаю. Это должно быть задокументировано в Yii. - person mae; 13.03.2019
comment
Плохое решение. Модель поиска будет иметь значения по умолчанию — представление сетки будет фильтроваться по умолчанию со значениями по умолчанию. - person Anton Rybalko; 26.04.2019
comment
@AntonRybalko Я не понимаю вашего комментария .. значения по умолчанию устанавливаются в CreatureForm (используется для управления записями), а НЕ устанавливаются в CreatureSearch (используется для GridView). Таким образом, GridView НЕ будет фильтроваться по умолчанию со значениями по умолчанию. - person Arth; 26.04.2019
comment
Ой, извините. Не заметил. - person Anton Rybalko; 02.05.2019

Есть два способа сделать это.

$model => new Model();

Теперь $model имеет все атрибуты по умолчанию из таблицы базы данных.

Или в ваших правилах вы можете использовать:

[['field_name'], 'default', 'value'=> $defaultValue],

Теперь $model всегда будет создаваться с указанными вами значениями по умолчанию.

Вы можете увидеть полный список основных валидаторов здесь http://www.yiiframework.com/doc-2.0/guide-tutorial-core-validators.html

person Coz    schedule 18.01.2017
comment
Как я указал в опции D, этого решения недостаточно, поскольку значение фактически не устанавливается до тех пор, пока не будет выполнено $model->validate(). - person mae; 18.01.2017

Я знаю, что на него ответили, но я добавлю свой подход. У меня есть модели Application и ApplicationSearch. В Application model добавляю init с проверкой текущего экземпляра. Если это ApplicationSearch, я пропускаю инициализацию.

    public function init()
    { 
        if(!$this instanceof ApplicationSearch)  
        {
            $this->id = hash('sha256',  123);
        }

        parent::init();
    }

также, как прокомментировал @mae ниже, вы можете проверить наличие метода поиска в текущем экземпляре, если вы не добавили какой-либо метод с поиском по имени в базовую модель без поиска, поэтому код становится таким:

    public function init()
    { 
        // no search method is available in Gii generated Non search class
        if(!method_exists($this,'search'))  
        {
            $this->id = hash('sha256',  123);
        }

        parent::init();
    }
person Stefano Mtangoo    schedule 19.12.2016
comment
Мне это нравится больше всего, и это дало мне еще лучшую идею. Вместо этого проверьте !method_exists($this,'search'), чтобы вам не нужно было знать имя дочернего класса. - person mae; 18.01.2017
comment
Извините, мне очень не нравится такой подход... вы вводите зависимость от дочернего класса от родителя. Дочерний элемент должен иметь возможность изменяться волей-неволей, не влияя на функциональность родителя, и любая функциональность, характерная для дочернего элемента, должна быть видна в дочернем элементе. Проверка метода с именем «поиск» еще хуже! - person Arth; 13.03.2019
comment
@Arth, учитывая ограничения вопроса, какое решение вы предлагаете? - person Stefano Mtangoo; 13.03.2019
comment
я добавил ответ - person Arth; 14.03.2019

Я прочитал ваш вопрос несколько раз и думаю, что есть некоторые противоречия.
Вы хотите, чтобы значения по умолчанию были доступны для чтения до и во время проверки, а затем пробуете init() или beforeSave(). Итак, если вы просто хотите установить значения по умолчанию в модели, чтобы они могли присутствовать в течение части жизненного цикла как можно дольше и не мешать производным классам, просто установите их после инициализации объекта.

Вы можете подготовить отдельный метод, в котором установлены все значения по умолчанию, и вызвать его явно.

$model = new Model;
$model->setDefaultValues();

Или вы можете создать статический метод для создания модели со всеми установленными значениями по умолчанию и возврата ее экземпляра.

$model = Model::createNew();

Или вы можете передать значения по умолчанию в конструктор.

$model = new Model([
    'attribute1' => 'value1',
    'attribute2' => 'value2',
]);

Это мало чем отличается от прямой установки атрибутов.

$model = new Model;
$model->attribute1 = 'value1';
$model->attribute2 = 'value2';

Все зависит от того, насколько прозрачной вы хотите, чтобы ваша модель была для вашего контроллера.

Таким образом, атрибуты устанавливаются для всего жизненного цикла, за исключением прямой инициализации, и это не мешает производной модели поиска.

person Bizley    schedule 04.09.2016
comment
Хорошо использовать ключевое слово Yii::createObject вместо new. - person meysam; 04.09.2016
comment
Почему? new намного быстрее. - person Bizley; 04.09.2016
comment
Yii::createObject позволяет делать больше, чем просто новое. Также, на мой взгляд, он сохраняет семантическую схему Yii2. Другие пользователи могут лучше понять ваш код, потому что все инициализации находятся в одном месте, а не в разных местах и ​​не разными методами. - person meysam; 04.09.2016
comment
Это правда, это для DI-контейнера, но здесь это излишество для такого простого случая. Ядро Yii 2 также не использует это во всех случаях — это влияет на производительность. - person Bizley; 04.09.2016
comment
Вы правильно поняли мой вопрос, но, к сожалению, предлагаемые вами решения просто неприменимы в реальном мире. Оператор (new MyModel)->attr должен возвращать значение по умолчанию attr. Весь смысл значений атрибутов по умолчанию в том, что нам не нужно каждый раз устанавливать их вручную. - person mae; 04.09.2016
comment
Так что насчет первых двух методов? - person Bizley; 04.09.2016
comment
У нас не всегда есть контроль над инициализацией, поэтому мы должны заставить ее работать на new MyModel без каких-либо дополнительных вызовов. Ваш второй метод немного лучше, но все же требует пользовательского вызова. Однако чтение вашего ответа дало мне новую идею. Как насчет того, чтобы использовать мой метод A) и добавить дополнительную проверку имени класса, чтобы она применялась только к родительскому классу, но не к его дочерним элементам? - person mae; 04.09.2016
comment
Не очень хорошо, потому что что, если вы хотите расширить класс, чтобы он получал значения по умолчанию, но не был классом поиска? - person Bizley; 04.09.2016
comment
Верно. Возможно, нам следует каким-то образом исключить дочерний класс поиска от получения значений по умолчанию (именно это Yii1.1 и сделал с unsetAttributes()) - person mae; 04.09.2016
comment
Давайте продолжим обсуждение в чате. - person mae; 04.09.2016
comment
Теперь метод называется loadDefaultValues() в Yii2. - person Razvan Grigore; 06.12.2019

Просто переопределите метод __construct() в своей модели следующим образом:

class MyModel extends \yii\db\ActiveRecord {

    function __construct(array $config = [])
       {
           parent::__construct($config);
           $this->attr = 'defaultValue';
       }
    ...
}
person Игорь Чугусов    schedule 12.05.2018
comment
Это также нарушает модели поиска (см. сценарий A). - person mae; 14.05.2018

Если вы хотите загрузить значение по умолчанию из базы данных, вы можете поместить этот код в свою модель.

 public function init()
 {
    parent::init(); 
    if(!method_exists($this,'search')) //for checking this code is on model search or not
    {
        $this->loadDefaultValues();
    }
 }
person Dedy Kurniawan    schedule 11.06.2021

Вы можете подготовить отдельный метод, в котором установлены все значения по умолчанию, и вызвать его явно.

$model = new Model;
if($model->isNewRecord())
    $model->setDefaultValues();
person Community    schedule 08.02.2017