Yii2 зарелизился не так давно, и рецепты в интернете не всегда полные.

Проблема default scope в Yii2 существует уже больше года, еще с того времени, как Yii2 был в бета версии. Соответственно при поиске нас кидает на различные обсуждения, среди которых преобладает не гибкое решение, препятствующее нормальному наследованию классов.

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

Не гибкое решение Default scope, которое преобладает при поиске, выглядит так:

Не используйте этот вариант:
class ActiveRecord extends \yii\db\ActiveRecord
{
    public static function find()
    {
        return parent::find()->andWhere(['active' => 1]);
    }
}



Если мы заходим добавить обычный scope, так как написано в документации (путем наследования от ActiveQuery), метод find будет переопределен.

Пример добавления scope из документации:
namespace app\models;

use yii\db\ActiveRecord;
use yii\db\ActiveQuery;

class Comment extends ActiveRecord
{
    public static function find()
    {
        return new CommentQuery(get_called_class());
    }
}

class CommentQuery extends ActiveQuery
{
    public function active($state = true)
    {
        return $this->andWhere(['active' => $state]);
    }
}



Гибкое решение для default scope

Правильная реализация Default scope — создать свой класс, наследованный от ActiveQuery, и возвращать его в методе find. Также в этом ActiveQuery можно прописывать другие scopes, как это описано в документации.

Гибкая реализация Default scope:
class MyActiveRecord extends \yii\db\ActiveRecord
{
    public static function find()
    {
        return new MyActiveQuery(get_called_class());
    }
}

class MyActiveQuery extends \yii\db\ActiveQuery
{
    public function init()
    {
        $modelClass = $this->modelClass;
        $tableName = $modelClass::tableName();
        $this->andWhere([$tableName.'.active' => 1]);
        parent::init();
    }

} 


Таким образом в наследуемых MyActiveRecord моделях мы будем иметь доступ ко всем scopes, а если захотим добавить еще default scope или обычные scopes, можно аналогично расширять MyActiveQuery.

UPDATE
Прошу прощения, первая моя статья.
Судя по комментариям у многих возник вопрос, к чему это все я написал и в чем разница.

Если мы прописываем default scope прямо в методе find, а потом в наследуемых классах задаем обычные скоупы в наследниках ActiveQuery, мы перетираем метод find родителя и его условия.

Проблемы возникают в подобных случаях:
  1. когда нужно задать scopes для моделей, у родителя которых задано условие default scope.
    Если делать как в варианте, который я назвал «не правильным», условие default scope будет затерто.
  2. когда у нас большой проект и в нем присутсвуют модули и подмодули.
    Часто для специфичных модулей нужны свои scopes и default scope, которые дополняют условия запросов родителей.

Получается, что нужно использовать гибкий вариант всегда, когда создается свой класс ActiveRecord, в котором нужно указать условие запроса к БД по умолчанию.
Т.к. почти всегда найдется другой программист (или вы через пол года), который захочет добавить скоупы в какой-то модели и сделает это как в документации.

Комментарии (17)


  1. Ekstazi
    06.05.2015 18:50
    +2

    Default scope на то и «default» чтоб быть по-умолчанию для всех запросов.


  1. zelenin
    06.05.2015 20:07

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


    1. vism Автор
      14.05.2015 11:24

      В простом случае разницы нет, действительно.
      Кейс по шагам:
      1) Для всех find к БД нужно добавить условие is_deleted <> 0
      2) Создаем свой класс MyActiveRecord в котором прописываем default scope одним из способов
      3) В какой-то модели нам понадобились скоупы и мы делаем

      как в документации
      namespace app\models;
      
      use yii\db\ActiveRecord;
      use yii\db\ActiveQuery;
      
      class Comment extends ActiveRecord
      {
          public static function find()
          {
              return new CommentQuery(get_called_class());
          }
      }
      
      class CommentQuery extends ActiveQuery
      {
          public function active($state = true)
          {
              return $this->andWhere(['active' => $state]);
          }
      }
      


      1. vism Автор
        14.05.2015 12:11

        я имел ввиду, что default scope затерт в «неправильном» варианте)


  1. JiLiZART
    06.05.2015 20:12

    Не убедительно. А плодить на каждую модель класс чтобы задать default scope не вижу смысла.


    1. vism Автор
      14.05.2015 11:26

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


  1. usualdesigner
    06.05.2015 21:40

    Я согласен, что предложенный в статье метод имеет место быть. Но как же прекрасно выглядел defaultScope(){} в прошлой версии.


  1. Rathil
    06.05.2015 23:04

    Тоже сталкивался с этой проблемой. Долго думал над её решением, предложенный Вами вариант тоже рассматривал, однако ещё до конца не решил как лучше делать (сейчас в отпуске и даю отдохнуть мозгу). Предложенный вариант не удовлетворяет на все 100? дефолт, так как если Вы будете использовать не andWhere, а просто where, лично я считаю, должно заменить все прежние условия, кроме дефолтного! Поэтому я ещё в раздумии, какой костыль изобрести или выбрать.


    1. Unclead
      07.05.2015 19:55

      Можно подключить требуемое условие в момент подготовки запроса к сборке

         /**
           * Выводим все записи у которх статус не в числе невидимых.
           *
           * @param \yii\db\QueryBuilder $builder
           * @return \yii\db\Query
           */
          public function prepare($builder)
          {
              $this->andWhere(['not in', $this->statusField, array_diff(
                  $this->hiddenStatuses,
                  $this->exceptedHiddenStatuses
              )]);
              return parent::prepare($builder);
          }
      


      1. vism Автор
        14.05.2015 11:34

        Интуитивно кажется, что так лучше.
        Надо поразмыслить, нет ли подводных камней…


    1. Zhuravljov
      07.05.2015 20:11

      Серебряной пули не существует ).
      Дефолтный скоуп — не всегда одно лишь дополнительное условие. Еще иногда нужно дополнить select, join и прочее. Просто скоупы нужно стараться составлять и применять таким образом, чтобы они дополняли базовый запрос но не перекрывали друг друга.


  1. xskif
    07.05.2015 01:39

    А чем вас не устраивает это:
    return new CommentQuery(get_called_class)->andWhere(...);


    1. xskif
      07.05.2015 01:41

      Забыл скобки вокруг new. С телефона не удобно.


    1. vism Автор
      14.05.2015 11:36

      Если будет наследование модели и там будут скоупы (соjтвественно создадим MyCommentQuery), find с условием перетрется


      1. xskif
        14.05.2015 14:36
        +1

        Если будет наследование модели и для них обоих нужно установить один и тот же default scope, то возможно стоит пересмотреть архитектуру. Конечно Ваше решение подойдет в таком варианте, как DRY, но мне кажется, что это лишняя сложность.

        В интернете советуют вообще отказаться от default scope, так как это может вызывать неожиданное поведение, и я с ними согласен. Я использую свои query классы, и если в двух из трех мест приложения мне надо написать один и тот же скоуп — это не WET. В крайнем случае всегда есть фабрика или SOA.

        Ну и если так уж нужны default scope в двух наследованных моделях, то почему бы логику default scope не засунуть в общий query класс?

        class BaseCommentQuery extends ActiveQuery {
          public function default () {
            return $this->andWhere(...);
          }
        }
        
        class CommentQuery extends BaseCommnetQuery {}
        
        class MyCommentQuery extends BaseCommentQuery {} // или extends CommentQuery
        
        // class Comment
        public static function find () {
          return new CommentQuery(get_called_class())->default();
        }
        
        // class MyComment
        public static function find () {
          return new MyCommentQuery(get_called_class())->default();
        }
        


        1. xskif
          14.05.2015 14:42

          P.S. кстати, если использовать ActiveQuery класс для задания скоупов и для вызова default scope внутри init(), как Вы предложили, — это, как мне кажется, нарушит SRP и SOLID соответственно. Придется лезть в класс и для того чтобы поправить default scope и для того чтобы изменять обычные scope. Комментатор ниже верно говорит:

          «Наследник от ActiveQuery определяет список скоупов, но не включает в себя их применение.»


  1. Zhuravljov
    07.05.2015 19:38

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