Правильное использование Yii

Вступление


На самом деле, в заголовке должен стоять знак вопроса. Довольно долго я не кодил как на yii, так и на php в целом. Сейчас, вернувшись, хочется переосмыслить свои принципы разработки, понять куда двигаться дальше. И лучший способ — изложить их и выложить на ревью профессионалам, что я и делаю в этом посте. Несмотря на то, что я преследую чисто корыстные цели, пост будет полезен многим новичкам, и даже не новичкам.

Оформление и понятия


В тексте понятия «контроллер» и «модель» будет встречаться в двух контекстах: MVC и Yii, обратите на это внимание. В неочевидных местах я буду пояснять какой контекст использую.
«Представление» — это представление в контексте MVC.
«Вью» — это файл из папки views.
Паттерны я буду выделять ЗАГЛАВНЫМИ буквами.

Поехали!



Yii — очень гибкий фреймворк. Это дает возможность некоторым разработчикам не заботиться о структуризации своего кода, что всегда ведет к куче багов и сложному рефакторингу. Впрочем, Yii здесь не при чем — довольно часто проблемы начинаются уже с банального недопонимания принципа MVC.

Поэтому в этом посте я рассмотрю основы MVC, и его C и V в контексте Yii. Буква М — это отдельная сложная тема, которая достойна своего поста. Все примеры кода будут банальными, но отражающими сущность принципов.

MVC


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

К сожалению, я не раз видел, когда выражение «Yii — это MVC фреймворк» принимали слишком дословно (то есть М — это CModel, С — это CController, V — это вьюхи из папки views), что уводит в сторону от понимания самого принципа. Это порождает массу ошибок, например, когда в контроллере выбираются все необходимые данные для вьюхи, или когда в контроллер выносятся куски бизнес-логики.

Контроллер («C») — это операционный уровень приложения. Не стоит путать его с классом CContrller. CContrller наделен многими обязанностями. В MVC понятие «контроллер» — это прежде всего экшн CController'а. В случае выполнения какой-либо операции над объектом, контроллер не должен знать как именно выполнять эту операцию — это задача «М». В случае отображения объекта он не должен знать как именно отображать объект — это задача «V». По факту, контроллер должен просто взять нужный объект(ы), и сказать ему(им) что делать.

Модель («М») — это уровень бизнес-логики приложения. Опасно ассоциировать понятие модели в Yii с понятием модели в MVC. Модель — это не только классы сущностей (как правило CModel). Сюда, например, входят специальные валидаторы CValidator, или СЛУЖБЫ (если они отображают бизнес-логику), РЕПОЗИТОРИИ, и многое другое. Модель ничего не должна знать об контроллерах или отображениях, использующих ее. Она содержит только бизнес-логику и ничего больше.

Представление («V») — уровень отображения. Не стоит воспринимать его как просто php файл для отображения (хотя, как правило, оно так и есть). У него есть своя, порой, очень сложная, логика. И если для отображения объекта нам нужны какие-то специфичные данные, например список языков или что-то еще, запрашивать их должен именно этот уровень. К сожалению, в Yii нельзя связать вьюху с каким-то определенным классом (разве что с помощью CWidget и т.п.), который бы содержал логику отображения. Но это легко реализовать самому (редко нужно, но иногда — крайне полезно).

Сам же Yii предоставляет нам шикарную инфраструктуру для всех этих трех уровней.

Типичные ошибки MVC



Приведу пару типичных ошибок. Эти примеры крайне утрированны, но они отображают суть. В масштабах крупного приложения эти ошибки вырастают в катастрофические проблемы.

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

    public function actionUserView($id)
    {
        $user = User::model()->findByPk($id);
        $posts = Post::model()->findAllByAttributes(['user_id' => $user->id]);
        $this->render('userWithPosts', [
            'user' => $user,
            'posts' => $posts
        ]);
    }

Здесь ошибка. Контроллер не должен знать о том, как именно будет отображаться пользователь. Он должен найти пользователя, и сказать ему «отобразись-ка с помощью вот этой вьюхи». Здесь же мы выносим часть логики отображения в контроллер(а именно — знание о том, что ей нужны посты ).

Проблема в том, что если делать как в примере — про повторное использование кода можно забыть и словить повсеместное дублирование.

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

Так же мы не сможем повторно использовать этот экшн. Если убрать из него выборку постов, а название вьюхи сделать параметром (например, реализовав его в виде CAction) — мы можем использовать его везде, где нужно отобразить какую-либо вьюху с данными пользователя. Это выглядело бы как-то так:

   public function actions()
    {
        return [

            'showWithPost' => [
                'class' => UserViewAction::class,
                'view' => 'withPost'
            ],

            'showWithoutPost' => [
                'class' => UserViewAction::class,
                'view' => 'withoutPost'
            ],

            'showAnythingUserView' => [
                'class' => UserViewAction::class,
                'view' => 'anythingUserView'
            ]
        ];
    } 


Если мешать контроллер и отображение — это не возможно.

Эта ошибка создает лишь дублирование кода. Вторая ошибка имеет куда более катастрофические последствия.

2. Допустим нам нужно перевести новость в архив. Делается это установкой поля status. Смотрим экшн:

    public function actionArchiveNews($id)
    {
        $news = News::model()->findByPk($id);
        $news->status = News::STATUS_ARCHIVE;
        $news->save();
    }


Ошибка данного примера в том, что мы переносим бизнес-логику в контроллер. Это так же ведет к невозможности повторно использовать код (ниже объясню почему), но это лишь мелочь по сравнению со второй проблемой: что если мы изменим способ перевода в архив? Например, вместо изменения статуса мы будем присваивать true полю inArchive? И это действие будет выполняться в нескольких местах приложения? И это не новость, а транзакция на 10млн$?

В примере эти места легко найти — достаточно сделать Find Usage для константы STATUS_ARCHIVE. Но если вы сделали это с помощь запроса "status = 'archive'" — найти гораздо сложнее, ведь даже один лишний пробел — и вы бы не нашли эту строку.

Бизнес логика всегда должна оставаться в модели. Здесь следует выделить отдельный метод в сущности, который переводит новость в архив (или как-то по другому, но именно в слое бизнес-логики). Этот пример — крайне утрирован, немногие допускают подобную ошибку.

Но в примере из первой ошибки тоже есть эта проблема, гораздо менее очевидная:

  $posts = Post::model()->findAllByAttributes(['user_id' => $user->id]);


Знания о том, как именно связанны Post и User — это тоже бизнес-логика приложения. Поэтому данная строка не должна встречаться ни в контроллере, ни в представлении. Здесь правильным решением было бы использования релейшена для User, или скоупа для Post:

        // релейшн
        $posts = $user->posts;
        
        // скоуп
        $posts = Post::model()->forUser($user)->findAll();  


Магия CAction


Контроллеры (в терминологии MVC, в терминологии Yii — экшены) — самая реюзабельная часть приложений. Они не несут в себе практически никакой логики приложения. В большинстве случаев их можно спокойно копировать из проекта в проект.

Посмотрим как же можно реализовать UserViewAction из примеров выше:

class UserViewAction extends CAction
{
    /**
     * @var string view for render
     */
    public $view;

    /**
     * @param $id string user id
     * @throws CHttpException
     */
    public function run($id)
    {
        $user = User::model()->findByPk($id);

        if(!$user)
            throw new CHttpException(HttpResponse::STATUS_NOT_FOUND, "User not found");

        $this->controller->render($this->view, $user);
    }
}


Теперь мы можем задавать любую вьюху в конфиге экшена. Это хороший пример реюзабельности кода, но он не идеален. Модифицируем код, чтобы он работал не только с моделью User, а с любым наследником CActiveRecord:

class ModelViewAction extends CAction
{
    /**
     * @var string model class for action
     */
    public $modelClass;

    /**
     * @var string view for render
     */
    public $view;

    /**
     * @param $id string model id
     * @throws CHttpException
     */
    public function run($id)
    {
        $model = CActiveRecord::model($this->modelClass)->findByPk($id);

        if(!$model)
            throw new CHttpException(HttpResponse::STATUS_NOT_FOUND, "{$this->modelClass} not found");

        $this->controller->render($this->view, $model);
    }
}


По сути мы просто заменили жестко заданный класс User на конфигурируемое свойство $modelClass В итоге получился экшн, который можно использовать для вывода любой модели с помощью любой вьюхи.

На первый взгляд он не гибок, но этот всего лишь пример для понимания общего принципа. PHP — очень гибкий язык, и это дает нам простор для творчества:

  • в свойство $view мы можем передать не строку, а анонимную функцию, которая вернет название вьюхи. В экшене проверять: если во $view строка — то это и есть вьюха, если callable — то вызывать его и получать вьюху.
  • сделать boolean свойство renderPartial и рендерить с помощью него, если надо
  • проверять заголовок на Accept: если html — рендерим вьюху, если json — отдаем json
  • много много всего другого


Подобные экшны можно написать практически для любого действия: CRUD, валидация, выполнение бизнес-операций, работа с связанными объектами и т.д.

На самом деле, достаточно написать порядка 30-40 подобных экшнов, которые покроют 90% кода контроллеров (естественно, если вы разделяете модель, представление и контроллер). Самым приятным плюсом, конечно, является уменьшение кол-ва багов, ибо гораздо меньше кода + проще писать тесты + когда экшн используется в сотне местах они всплывают гораздо быстрее.

Пример экшна для Update


Приведу еще пару примеров. Вот экшн на update

class ARUpdateAction extends CAction
{
    /**
     * @var string update view
     */
    public $view = 'update';

    /**
     * @var string model class
     */
    public $modelClass;

    /**
     * @var string model scenario
     */
    public $modelScenario = 'update';

    /**
     * @var string|array url for return after success update
     */
    public $returnUrl;


    /**
     * @param $id string|int|array model id
     * @throws CHttpException
     */
    public function run($id)
    {
        $model = CActiveRecord::model($this->modelClass)->findByPk($id);

        if($model === null)
            throw new CHttpException(HttpResponse::STATUS_NOT_FOUND, "{$this->modelClass} not found");

        $model->setScenario($this->modelScenario);

        if($data = Yii::app()->request->getDataForModel($model))
        {
            $model->setAttributes($data);

            if($model->save())
                Yii::app()->request->redirect($this->returnUrl);
            else
                Yii::app()->response->setStatus(HttpResponse::STATUS_UNVALIDATE_DATA);
        }

        $this->controller->render($this->view, $model);
    }
}


Его код я взял из CRUD gii, и немного переработал. Помимо того, что введено свойство $modelClass для реюзабельности, он дополнен еще несколькими важными моментами:

  • Установку scenario для модели. Это крайне важный момент, о котором многие забывают. Модель должна знать что с ней собираются делать! Подробнее об этом я напишу в следующем посте, посвященный моделям.
  • Получение данных не из $_POST, а с помощью Yii::app()->request->getDataForModel($model), ибо данные могут придти в json формате, или как-то по другому. Знания о том, в каком формате приходят данные и как их правильно распарсить — это не задача контроллера, это задача инфраструктуры, в данном случае — HttpRequest.
  • В случае непрохождения валидации (которая находиться в методе save) устанавливается http статус STATUS_UNVALIDATE_DATA. Это очень важно. В стандартном варианте код вернул бы статус 200 — что означает «все хорошо». Но это же не так! Если, например, клиент определяет успешность выполнения операции по http статусу, то это вызвало проблемы. А так как мы не знаем, как именно будет работать клиент, нужно соблюдать все правила http протокола.


Естественно, этот контроллер намного проще реального:

  • $view и $retrunUrl — просто строки (для гибкости их лучше сделать string|callable)
  • не проверяется заголовок Accept чтоб понять в каком виде выводить данные и делать ли редирект или просто выводить json
  • Жестко задан метод модели для сохранения. Например гибче было бы сделать так: $model->{$this->updateMethod}()
  • многое другое


Еще один важный момент который здесь опущен — приведение входных данных к необходимым типам. Сейчас данные обычно присылаются в json, что частично облегчает задачу. Но проблема все равно остается, например, если клиент шлет timestamp, а в модели — MongoDate. Предоставить модели правильные данные — это определенно задача контроллера. Но информация о том, какие типы у полей — это знания класса модели.

На мой взгляд, наилучшее место выполнения приведения — метод Yii::app()->request->getDataForModel($model). Получить типы полей можно несколькими способами, для меня самые привлекательные — это:

  • Если у нас AR — то мы можем получить эти сведения из схемы таблицы.
  • Сделать в модели метод getAttributesTypes, который вернет информацию о типах.
  • Рефлексия, а именно — получение с помощью CModel::getAttributeNames списка атрибутов, затем обход их рефлексией с целью парсинга комментария к полю и вычисления типа, сохранение это в кэш. К сожалению, нормальных аннотаций в php нет, так что это довольно спорный способ. Но он избавляет от написания рутины.


В любом случае, мы можем сделать интерфейс IAttributesTypes где определить метод getAttributesTypes, и объявить метод HttpRequest::getDataForModel как public getDataForModel(IAttributesTypes $model). А каждый класс пусть сам определяет как ему реализовывать интерфейс.

Пример экшна для List


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

class MongoListAction extends CAction
{
    /**
     * @var string view for action
     */
    public $view = 'list';

    /**
     * @var array|EMongoCriteria predefined criteria
     */
    public $criteria = [];

    /**
     * @var string model class
     */
    public $modelClass;

    /**
     * @var string scenario for models
     */
    public $modelScenario = 'list';

    /**
     * @var array dataProvider config
     */
    public $dataProviderConfig = [];

    /**
     * @var string dataProvuder class
     */
    public $dataProviderClass = 'EMongoDocumentDataProvider';

    /**
     * @var string filter class
     */
    public $filterClass;

    /**
     * @var string filter scenario
     */
    public $filterScenario = 'search';

    /**
     *
     */
    public function run()
    {
        // Первым делом создадим фильтр и установим параметры фильтрации из входных данных
        /** @var $filter EMongoDocument */
        $filterClass = $this->filterClass ? $this->filterClass : $this->modelClass;
        $filter = new $filterClass($this->filterScenario);
        $filter->unsetAttributes();
        if($data = Yii::app()->request->getDataForModel($filter))
            $filter->setAttributes($data);
        $filter->search(); // Этот метод для того, чтобы критерия модели фильтра стала выбирать по установленным в модели атрибутам

        // Теперь смержим критерию фильтра с предустановленной критерией
        $filter->getDbCriteria()->mergeWith($this->criteria);

        // Теперь создадим дата провайдер. Дата провайдер из расширения yiimongodbsuite может брать критерию из
        // переданной ему модели (в нашем случае - фильтра)
        /** @var $dataProvider EMongoDocumentDataProvider */
        $dataProviderClass = $this->dataProviderClass;
        $dataProvider = new $dataProviderClass($filter, $this->dataProviderConfig);

        // Теперь установим сценарии для моделей. Этот метод я опущу, он просто обходит модели и ставит каждой сценарий
        self::setScenario($dataProvider->getData(), $this->modelScenario);

        // И выводим
        $this->controller->render($this->view, [
            'dataProvider' => $dataProvider,
            'filter' => $filter
        ]);
    }

}



И пример его использования, выводящий неактивных юзеров:

    public function actions()
    {
        return [
            'unactive' => [
                'class' => MongoListAction::class,
                'modelClass' => User::class,
                'criteria' => ['scope' => User::SCOPE_UNACTIVE],
                'dataProviderClass' => UserDataProvider::class
            ],
        ];
    }


Логика работы проста: получаем критерию фильтрации, делаем дата-провайдер и выводим.

Фильтр:

Для простой фильтрации по значением атрибутов достаточно использовать модель того же класса. Но обычно фильтрация гораздо сложнее — в ней может быть своя очень сложная логика, которая вполне может делать кучу запросов к БД или что-то еще. Поэтому иногда разумно унаследовать класс фильтра от модели, и реализовать эту логику там.

Но единственное назначение фильтра — получение критерии для выборки. Реализация фильтра в примере — не совсем удачная. Дело в том, что несмотря на возможность установить класс фильтра (с помощью $filterClass), она все равно подразумевает что это будет СModel. Об этом свидетельствуют вызов методов $filter->unsetAttributes() и $filter->search(), которые присуще моделям.

Единственное что фильтру нужно — это получать входные данные и отдавать EMongoCriteria. Он просто должен реализовывать этот интерфейс:

interface IMongoDataFilter
{
    /**
     * @param array $data
     * @return mixed
     */
    public function setFilterAttributes(array $data);

    /**
     * @return EMongoCriteria
     */
    public function getFilterCriteria();
}


Filter в названиях методов я вставил чтоб не зависеть от декларации методов setAttributes и getDbCriteria в имплементирующем классе. Чтобы использовать модель в качестве фильтра, лучше всего написать простенький трейт:

trait MongoDataFilterTrait
{
    /**
     * @param array $data
     * @return mixed
     */
    public function setFilterAttributes(array $data)
    {
        $this->unsetAttributes();
        $this->setAttrubites($data);   
    }

    /**
     * @return EMongoCriteria
     */
    public function getFilterCriteria()
    {
        if($this->validate())
            $this->search();

        return $this->getDbCriteria();
    }
}


Переписав экшн под использование интерфейса, мы бы могли использовать любой класс, который реализует интерфейс IMongoDataFilter, не важно модель это или что-то другое.

Дата-провайдер:
Все что касается логики выборки необходимых данных — за это отвечает дата-провайдер. Порой он содержит так же довольно сложную логику, поэтому имеет смысл конфигурировать его класс с помощью $dataProviderClass.

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

Логику подгрузки можно разместить и в каком-нибудь классе-РЕПОЗИТОРИИ, но если в обязанности конкретного дата-провайдера входит возвращение данных вместе с релейшенами, вызывать метод-подгрузчик РЕПОЗИТОРИЯ должен именно дата-провайдер. О реюзабельности дата-провайдеров я напишу ниже.

Критерия в использовании экшена:


Я хочу еще раз обратить внимание на самую «багогенерирующую» проблему:

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

В примере использования экшена все сделано правильно. С помощью скоупа мы указали кого хотим вывести, но сам скоуп находиться в модели.

На самом деле, скоуп — это «кусочек» СПЕЦИФИКАЦИИ. Можно легко переписать экшн чтоб работал с спецификациями. Хотя, это востребовано только в сложных приложениях. В большинстве случаев, скоуп — идеальное решение.

Про разделение контроллера и представления:


Иногда полностью отделять представление от контроллера нецелесообразно. Например, если для вывода списка нам необходимы только несколько атрибутов модели — глупо выбирать весь документ. Но это особенности конкретных экшенов, которые настраиваются с помощью конфигурирования (в данном случае — заданием select у критерии). Самое главное что мы вынесли эти настройки из кода экшенов, сделав их реюзабельным.


Связка экшна с классом модели


В большинстве случаев контроллер (именно CController) работает с одним классом (например с User). В таком случае, нет особой нужды в каждом экшене указывать класс модели — проще указать его в контроллере. Но в экшене эту возможность оставить необходимо.
Чтобы разрулить эту ситуацию, в экшене нужно прописать геттер и сеттер для $modelClass. Вид геттера будет вот таким:


    public function getModelClass()
    {
        if($this->_modelClass === null)
        {
            if($this->controller instanceof IModelController && ($modelClass = $this->controller->getModelClass()))
                $this->_modelClass = $modelClass;
            else
                throw new CException('Model class must be setted');
        }

        return $this->_modelClass;
    }


В принципе, можно сделать даже заготовку контроллера для стандартного CRUD:

/**
 * Class BaseARController
 */
abstract class BaseARController extends Controller implements IModelController
{
    /**
     * @return string model class
     */
    public abstract function getModelClass();

    /**
     * @return array default actions
     */
    public function actions()
    {
        return [
            'list' => ARListAction::class,
            'view' => ARViewAction::class,
            'create' => ARCreateAction::class,
            'update' => ARUpdateAction::class,
            'delete' => ARDeleteAction::class,
        ];
    }
}


Теперь мы можем делать CRUD контроллер в несколько строк:

class UserController extends  BaseARController
{

    /**
     * @return string model class
     */
    public function getModelClass()
    {
        return User::class;
    }
}



Итог по контроллерам


Большой набор гибко настраиваемых экшнов сокращает дублирование кода. Если разбить классы экшенов на четкую структуру (например, экшн по редактированию CActiveRecord и EMongoDocument отличаются лишь способом выборки объектов) — дублирования можно практически избежать. Такой код гораздо проще рефакторить. И в нем труднее сделать баг.
Конечно, подобными экшнами нельзя покрыть абсолютно все потребности. Но их значительную часть — однозначно да.

Представление


Yii дает нам шикарную инфраструктуру для ее построения. Это CWidget, CGridColumn, CGridView, СMenu и много другого. Не надо бояться все это использовать, расширять, переписывать.

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

Выше я упоминал, что контроллер не должен знать как именно будет отображаться сущность, поэтому он не должен содержать кода для выборки данных для вьюх. Я прекрасно осознаю, что данное заявление вызовет массу протестов — все всегда подготавливают данные в контроллерах. Даже сам Yii нам как бы намекает что контроллер и вьюха связанны, передавая во вьюху экземпляр контроллера в качестве $this.

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

Рассматривать я буду два общих случая: представление сущности со связанными данными, и представление списка сущностей. Примеры тривиальны, но суть объяснят.

Допустим, у нас есть интернет-магазин. Есть клиент (модель Client), его адрес (модель Address) и заказы (модель Order). Один клиент может иметь один адрес и много заказов.

Представление сущности со связанными данными


Допустим, нам нужно вывести инфу о клиенте, его адресе, и список его заказов.

По сути, каждая вьюха имеет свой собственный «интерфейс». Это передаваемые ей данные из CController::render и сам экземпляр контроллера (доступный по $this). Чем меньше данных ей передается — тем лучше, ибо тем более она независима. Такой подход позволит сделать вьюху реюзабельной в рамках проекта. Особенно учитывая, что в Yii вьюхи спокойно вкладываются друг в друга, и даже могут «общаться» между собой, например, с помощью CController::$clips.

Необходимо-достаточными данными для вывода нашей вьюхи — объект клиента. Имея его, мы спокойно получим все остальные данные.

Здесь следует сделать отступление и обратить внимание на букву «М» из MVC.

В каждой предметной области есть свои сущности и связи между ними. И очень важно, чтобы наш код максимально идентично их отображал.
В нашем магазине клиенту принадлежат и адрес и заказ. Это значит что в модели Clients мы должны явно отобразить эти связи с помощью свойств $client->adress или методов $client->getOrders()
Это очень важно. Подробнее об этом я расскажу в следующем посте.


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

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

        $this->widget('zii.widgets.CDetailView', [
            'model' => $client,
            'attributes' => [
                'fio',
                'age',
                'address.city',
                'adress.street'
            ]
        ]);

        foreach($client->orders as $order)
        {
            $this->widget('zii.widgets.CDetailView', [
                'model' => $order,
                'attributes' => [
                    'date',
                    'sum',
                    'status',
                ]
            ]);
        }



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

        $this->renderPartial('_client', $client);
        $this->renderPartial('_address', $client->address);
        $this->renderPartial('_orders', $client->orders);



Этот код прост, но имеет недостаток — если у клиента много заказов, нужно выводить его с пагинацией.
Никто не мешает нам запихнуть все это в дата провайдер. Допустим, модель Order — это монго-документ. Заворачивать будем в EMongoDocumentDataProvider:

        $this->widget('zii.widgets.grid.CGridView', [
            'dataProvider' => new EMongoDocumentDataProvider((new Order())->forClient($client)),
            'columns' => ['date', 'sum', 'status']
        ]);


Создание дата-провайдера во вьюхе несколько непривычно. Но на самом деле здесь все на месте: Контроллер свои обязанности уже отработал, знание о том как связанны Client и User находятся в предметной области (благодаря скоупу forClient), а знание о том как отображать данные находятся во вьюхе.

В действительности, некоторые мои коллеги, увидев это, крутили у виска — создание дата-провайдера в вьюхе — что за бред? При этом сами выполняли подобные действия в виджетах, не осознавая что виджет — это, в первую очередь, инфраструктура представления.

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

Представление списка сущностей


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

Допустим, что Client, Address и Order — это три разных коллекции в MongoDB. В случае вывода одного клиента, мы спокойно можем вызвать $client->address. Это сделает запрос к БД, но это неизбежно.

Если мы выберем 100 клиентов, и для каждого вызовем $client->address — мы получим 100 запросов к БД — это неприемлемо. Загружать адреса нужно для всех клиентов разом.

Если бы мы использовали AR, мы описали бы релейшены, и использовали их в критерии экшна. Но с MongoDB (точнее, с расширением yiimongodbsuite ) это не пройдет.

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

Делается это как-то так:

class ClientsDataProvider extends EMongoDocumentDataProvider
{
    /**
     * @param bool $refresh
     * @return array
     */
    public function getData($refresh=false)
    {
        if($this->_data === null || $refresh)
        {
            $this->_data=$this->fetchData();

            // Соберем список id адресов
            $addressIds = [];
            foreach($this->_data as $client)
                $addressIds[] = $client->addressId;

            // Выберем адреса
            $adresses = Address::model()->findAllByPk($addressIds);

            ... перебор клиентов и адресов и присвоение клиентам их адреса ....

        }
        return $this->_data;
    }
}



Тут есть 2 проблемы:

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


Решение — переместить код подгрузки в РЕПОЗИТОРИЙ, которым может являться сам класс модели.

Если мы переместим его туда, то наш дата-провайдер будет выглядеть вот так:

class ClientsDataProvider extends EMongoDocumentDataProvider
{
    /**
     * @param bool $refresh
     * @return array
     */
    public function getData($refresh=false)
    {
        if($this->_data === null || $refresh)
        {
            $this->_data=$this->fetchData();
            Client::loadAddresses($this->_data);
        }
        return $this->_data;
    }
}


Теперь все находиться на месте.

Отступление к «М»:
В качестве РЕПОЗИТОРИЯ мы могли использовать как класс Client, так и Address. Но существует четкая причина, почему я использовал именно Client. В нашей предметной области адрес абсолютно не важен вне контекста пользователя. Несмотря на то, что адрес имеет и свою коллекцию, и свой класс, логически он — всего лишь ОБЪЕКТ-ЗНАЧЕНИЕ. Поэтому он не должен знать ничего о том, кому принадлежит. Размещая код подгрузки адресов в Client, мы избавляемся от двухсторонней связи классов. А это всегда хорошо.


Реюзабельность дата-провайдеров


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

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

Контроллер как $this в вьюхах


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

Заключение


Все примеры — сильно упрощены. Реальные класс контроллеров, структуры моделей намного масштабны.

Это слишком сложно и запутанно — многие так подумают. Многие, сев работать за подобный код, не разобравшись в структуре, просто вырежут его и напишут «по простому».

Это вполне понятно — я всего лишь описал взаимодействие нескольких классов — а уже дикая путаница, простейший в реализации код раскидан по куче файлов. Но на самом деле — это четкая и логичная структура классов, в которых каждая строчка находиться именно на своем месте.

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

Послесловие


Несмотря на то, что пост называется «как правильно делать», он не претендует на правильность. Я и сам не знаю как правильно. Он — попытка донести, что нам нужно более осмысленно подходить к проектированию классов и их взаимодействию.

Разработчики PHP подарили нам мощнейший язык. Разработчики Yii подарили нам великолепнейший фреймворк. Но посмотрите вокруг — представители других языков и фреймвороков считают нас быдлокодерами. PHP и Yii — мы позорим их.

Своим халатным отношением к проектированию, банальным незнанием основных принципов MVC, объектно-ориентированного проектирования, языка, на котором пишем, и фреймворка, который используем — всем этим мы подводим PHP. Мы подводим Yii. Мы подводим компании на которые работаем, и которые обеспечивают нас. Но самое главное — мы подводим себя.

Задумайтесь.

Всем добра.
Share post

Comments 54

    +4
    А как быть с тем, что код с примером из пункта 1 генерирует сам webapp?
    Или с тем, что похожий код генерирует gii?
    А сотни примеров кода из гайдов?
    Сказать Qiang Xue, что он злобный буратино, предавший каноны MVC?
      +3
      Почему бы и нет? Первый Yii уже давольно стар (или зрел, кому как нравится) и всякое «исторически сложилось» там вполне может существовать.
      В yii2 они сообразили сделать отдельный класс View и это, имхо, хорошо.
        +3
        Обязательно сказать, только аргументировано и с примерами. А ещё лучше в виде pull-request.
          –2
          И ответит он что то типа: «Я разработчик, мне виднее. Идите в лес»?
            +1
            Скорее нет, чем да. Особенно при хорошей аргументации. К тому же, Qiang не единственный разработчик в команде Yii.
        +4
        Ааарр!!! Я в свое время писал на Yii довольно крупный проект. В целом все удобно и просто, но что меня больше всего бесит в Yii — это прямое присваивание публичных полей в коде примеров. Подобный подход очень очень очень очень небезопасен! Ваши данные могут быть изменены (т.к. mutable) каким-нибудь багом внутри Yii и поломать всю бизнес логику и даже данные в базе.
          +3
          Возможно, я не достиг уровня «будды» в программировании, но, если честно, я не понимаю повсеместного использования геттеров-сеттеров. Как бы да, в некоторых местах это необходимо. Например, критерия может быть в двух видах: CDbCriteria и array. В таком случае логично переписать сеттер который приводил бы тип к нужному. Но абсолютно везде — имхо, это лишний код и тупо усложняет.

          Что касается приведенного кода, то, во-первых, код чисто для примера, и многое там опущено дабы не усложнять. Во-вторых, там мы не присваиваем никаких безопасных свойств: все присвоения атрибутов модели идут через setAttributes, который в свою очередь смотрит на валидаторы, и при сохранении проверяет валидность. Другие присвоения (например, присвоение критерии EMongoDocumentDataProvider) реализованный внутри соответствующего расширения и нам не надо заботиться об этом. Присвоения свойств экшена — это уже чисто на нашей совести. Но Yii нам помогает: если мы присвоим несуществующую вьюху или скоуп — он нам однозначно даст нам об этом знать.

          Насчет «багов Yii» и «поломает бизнес логику и данные»: Что касается багов Yii, то я доверяю разработчика Yii, и баг Yii — это в какой-то степени форс-мажор. Но, естественно на «безбаговость» не приходиться рассчитывать — сломаться может не только Yii, а любой экстеншн, или даже собственный код. Для этого у нас есть пост-валидация бизнес-операций. И в случае ошибки — откатывать всю бизнес-операцию, с помощью транзакций или как-то по другому. В этом посте я не описал эту тему, ибо он и так слишком большой, плюс тема бизнес-операций — это тема «М». Это все я опишу в следующем посте.
            0
            Повсеместные геттеры и сеттеры — это страховка от изменения бизнес-логики. Если логика поменялась, нам не надо искать по всему проекту, где используется то или иное поле — доработка геттера устраняет все возможные проблемы. По поводу побочных эффектов — «лишний» код легко генерируется, а усложнение — да, в некоторой степени, но в первую очередь для человека, который сам так не делает. Если человек сам привык передавать всё через геттеры и сеттеры, код в таком стиле не доставляет никаких неудобств.
            В качестве приятного бонуса — геттеры и сеттеры делают код более инкапсулированным и тем самым положительно влияют на архитектуру и карму.
              +7
              В Yii есть поддержка обращения к виртуальным полям через геттер или сеттер. То есть:

              class X extends CComponent
              {  
                public function setField($value)
                {
                  // ...
                }
              
                public function getField($name)
                {
                  // ...
                }
              }
              
              $x = new X();
              $x->field = 'test';
              echo $x->field;
              


              Это позволяет везде использовать поля и вводить геттеры и сеттеры прозрачно как только они реально понадобятся.
                0
                Вообще, конечно, хотелось бы, чтобы все поля были приватными по умолчанию, и только если нужно — меняется на public или пишутся геттеры-сеттеры. Причем, логика прав доступа должна распространяться не только на прямой доступ к полю, но и на методы вида updateAttributes(array newAttrs) (или как оно там называется в Yii). Но понимаю, что такое решение будет непопулярным.
                  0
                  Это здорово, но потерю типизации никто не отменял.

                  И если для сеттера можно сделать проверку на уровне языка:
                  public function setUser(User $user) { ... }
                  то для обращения через виртуальные поля требуют ручной валидации кода.
                    +2
                    Вроде ровно наоборот получается. Пока поле обычное паблик свойство, то типизации нет, но как только мы переводим его в виртуальное (пишем сеттер и делаем его приватным или вообще меняем логику хранения), то получаем и типизацию, поскольку обращение пойдёт через сеттер.
                  –3
                  Если логика поменялась, нам не надо искать по всему проекту, где используется то или иное поле — доработка геттера устраняет все возможные проблемы.
                  Поискать по всему проекту не сложно — достаточно 2 кнопки нажать.
                0
                На меня конечно же в некоторой степени повлияло несколько лет программирования на Java и Scala.
              –6
              Если предметная область правильно спроектирована, у нас всегда будет простой способ получить связанные данные. И это абсолютно решает проблему с тем, что контроллер нам не передал список заказов.

              и далее
              foreach($client->orders as $order)
              и Yii вам нагенерит по 1 запросу к БД на каждый заказ.
                +6
                Это не так, ибо полностью зависит от реализации $client->orders. Например, если он реализован с помощью релейшена AR — выборка будет единоразовой. В трейте для релейшенов yiimongodbsuite, который я написал, данные так же кешируется.

                Если ArrayAccess написан так, что при каждой итерации будет производиться запрос — вы правы. В любом случае, это полностью лежит совести разработчика.
                  –4
                  Если в AR-модели есть релейшн orders, то загрузка не будет ленивой только в случае выборки с with('orders').
                    0
                    Автор прав, вы путаете с чем-то таким:
                    foreach(Client::model->findAll() as $client) {
                        $order = $client->order;
                        ...
                    }
                    
                      0
                      Зависит от реализации. И даже в случае если загрузка ленивая, то запрос может быть только один дополнительный типа SELECT * FROM order WHERE id IN (<список айдишников полученных ранее>)
                    0
                    И иногда это является единственным способом разгрузить БД ничего в ней не меняя.
                    0
                    Круто, спасибо! Взял у вас пару рецептиков.
                      +14
                      Первый пример очень спорный. Вы просто перекидываете дублирование кода с одного места на другое. Вдобавок, в рамках MVC вьюшка должна заниматься только тем, что следует из её названия — показывать пришедшие данные. А выборки, фильтрацию и прочее — задача модели и контроллера. Вы, по сути, делаете ненужным контроллер как таковой. Это уже ближе к идеалогии MV-V-M, гда ваш контроллер является просто связующим.
                      На мой взгляд, самым грамотным решением будет такая архитектура, при которой View принимает обобщенный список (допустим, User::model()), в котором контроллер уже выставил свои ограничения. А View уж как-нибудь сам разребется, как извлекать данные.
                      Например, у нас есть список товаров: Goods. Нам нужно показать только те, у которых цена больше какой-то. В контроллере: Goods->AddFilter('price', '>', $price); и вызывать необходимое представление. Оно может быть общим для любого количества товаров. Предсталение вообще ничего не должно знать о том, с какими параметрами его запросили. И с таким подходом контроллеру не важно, как работает представление внутри — главное, чтобы обрабатывало именно этот класс
                        –6
                        Насчет первого примера ошибаетесь. Автор как раз все правильно описал. Задача получения постов просто уходит в модель один раз и вызывается когда нужно, а не решается каждый раз в контроллере. Представление же просто обращается к постам пользователя $user->posts, причем даже более логично чем просто $posts.
                        Насчет правильной архитектуры вопрос, на мой взгляд, субъективный для каждой задачи. Здесь мне кажется не нужно закапываться в идеологию, когда можно сделать проще для маленькой задачи или проекта.
                          +2
                          $user->posts

                          А теперь добавьте пагинацию, сортировку по рейтингу, фильтр по какому-нибудь критерию. Через реляцию уже не получается(
                          Так или иначе эти условия должен задать контроллер. Контроллер может вернуть настроенную модель, массив постов или dataProvider. А что именно он вернет — суть дела не сильно меняет. Хотя вызов find() во вьюхе мне определенно не нравится.

                          userWithPosts.php:
                          <?php
                          $this->renderPartial('user', ['user' => $user]);//Эта вьюха понятия не имеет про посты пользователя
                          $this->renderPartial('posts', ['posts' => $posts]);//А эта - не знает ничего, кроме постов. И скорее всего предпочитает CDataProvider
                          

                          Таким не хитрым образом мы проводим декомпозицию задачи, получаем гибкое и повторно-используемое решение без лишней копипасты.
                            +1
                            Мне кажется вы не там ищете гибкость. На конкретную задачу используется своя вьюха, у нее смысл такой: отобразить данные по-своему. А в вашем примере, достаточно добавить одно условие к конкретной странице и она уже не такая гибкая, допустим на странице пользователя отображать посты без количества комментариев, а на странице постов с ними.
                            В любом случае, как я уже выше писал, любой подход не панацея, нужно во-первых использовать мозг и только потом шаблоны.
                              0
                              Серебрянную пулю я и не обещал. Здесь просто буква S из SOLID.

                              А в вашем примере

                              Равно как и в вашем и у ТС ;) Каждый случай индивидуален.
                          0
                          Вы открыли CDataProvider, он работает именно так.
                          В вашем случае:
                          $dp = new CDataProvider('Goods', $criteria);
                          $this->render('view', ['dataProvider' => $dp]);

                          В остальном же, полностью согласен
                            +2
                            >>выборки, фильтрацию и прочее — задача модели и контроллера.
                            Популярное заблуждение, связанное с повсеместным внедрением ОРМ с автоматизированными квери-билдерами. Против SQL в контроллере любой немедленно возмутится, однако, когда тот же SQL в том же контроллере написан неявно — все почему-то довольны.

                            На самом деле, выборки и фильтрации делают тоже модели. Контроллер может только сказать модели, что он хочет — причем (это важно!) в терминах бизнес-логики, а не в терминах полей в таблице.
                              +3
                              На самом деле, выборки и фильтрации делают тоже модели.

                              Популярное заблуждение :), связанное с повсеместным введением паттерна ActiveRecord, в котором сочетаются две ответственности: моделирование бизнес-логики и обеспечение персистентности. Да, на модель можно повесить функции фильтрации без знаний контроллера о её внутренней реализации, то тогда задачей контроллера становится загрузка всех данных в модель, а уж она будет там фильтровать в памяти в каком нибудь методе типа getPublicPost(), но обычно рационально выбрать из БД хранилища только посты с флагом is_public (если анализируя вызов контроллера, зная о реализации модели и вьюхи, мы понимаем что другие не понадобятся). Задача (одна из) контроллера подготовить модель к совершению операций бизнес-логики и/или отображению. В случае максимально изолированной модели (POPO) это означает либо подготовка всех необходимых данных для модели в контроллере, либо в передаче ей сущности (репозиторий, сервис, дата-провайдер и т. п.), которая ей эти данные предоставит (в терминах бизнес-логики). И даже если совмещать в одном классе хранение и логику (условно контроллер как бы передает $this как сервис хранения), то вызывать из метода бизнес-логики функции фильтрации этого сервиса означает опять смешивать ответветственности не только в рамках класса, но и в рамках метода, а также писать завуалированные sql запросы в них. :)
                              Против SQL в контроллере любой немедленно возмутится, однако, когда тот же SQL в том же контроллере написан неявно — все почему-то довольны.

                              Спорно. Не любой возмутится даже против явного, не говоря о неявном. Задача контроллера проинициализировать модель перед вызовом метода бизнес-логики и/или отображением, а потом сохранить состояние, если оно изменилось. Методы бизнес-логики (или геттеры для представлений) не должны дергать БД или иное хранилище напрямую, а значит контроллер или должен обеспечить модель всеми нужными данными до их вызова (явный или неявный SQL), либо предоставить ей ссылку на сущность, которая это позволяет (пускай даже на саму себя). Но в методе бизнес-логики дергать явный или неявный sql не есть гуд, имхо — данные или уже должны быть, или sql должен дергаться как минимум за одним уровнем абстракции в виде метода, который из терминов бизнес-логики (это важно! :) соберет запрос к базе, пускай даже этот метод в том же классе находится.
                                0
                                Нет.

                                Если говорить об Active Record, то он и состоит в объединении бизнес-логики и логики работы с хранилищем в рамках класса модели (паттерн или антипаттерн это — отдельный разговор, не относящийся к MVC).

                                Если говорить о Data Mapper, то DM тоже является частью «M» в MVC, но никак не является частью контроллера.
                                  +2
                                  Data Mapper не является никакой частью MVC. Он вообще вне ее, т.к. это инфраструктурный слой, а MVC это слой приложения.
                                    0
                                    Ну это смотря как посмотреть. Вы же не станете использовать DM во view?
                                      0
                                      Если вдруг на странице пользователя сказали вывести нечто к пользователю не относящееся, типа список последних десяти постов, то почему нет? В контроллер UserController::Show это запихивать?
                                        0
                                        В сервис.
                                          +1
                                          А обращаться к нему где?
                                          0
                                          В описанном в статье подходе это можно легко провернуть, внедрив в шаблон виджет, которому передали data provider.
                                            0
                                            Но от этого дата провайдер не становится частью модели, он часть системы хранения.
                                              0
                                              Верно. Модель, если исходить из ее общепринятого определения, как упрощенного образа реального объекта, точно не может себя выбирать. Так что обязанность выборки и остальных операций снимается с нее.
                                                +1
                                                Этого часто не понимают, особенно при использовании ActiveRecord — поместить метод выборки с фильтрацией или сохранения в класс модели ещё куда ни шло, но вот писать код фильтрации или сохранения прямо в методах бизнес-логики — как говорится, руки бы поотрывать. Вот из недавнего — есть задача, к ней могут быть комментарии. Хранится в виде таблиц task и task_comment со связью один-ко-многим, маппится на классы Task и TaskComment соответственно. Есть метод Task::addComment($commentString), который вызывается из контроллера, а в нём код типа $commentRow = (new TaskComment)->setContent($commentString)->save(); то есть бизнес-операция (создание сущности и тсановка её свойств) и операция сохранения дергаются в одном методе и контроллер должен об этом знать.
                                      0
                                      Пока они объединены лишь в рамках класса, то это ещё более-менее нормально (разделение бизнес-логики и логики хранения осуществляется по методам). Как только только это смешивается в одних методах, то поддержка, рефакторинг, повторное использование и т. п. превращаются в ад в проектах посложнее «бложика».
                                        0
                                        Я согласен, что AR — проблемный паттерн. Но это не причина переносить в контроллер хоть какую-либо логику, кроме инициализации и связывания M и V. Для загрузки моделей по критериям есть Data Mapper, для сложной «обвязочной» работы — сервисы (и вот их уже можно считать частью C, но сервисы тоже ничего не знают о полях в базе ;).
                                          0
                                          Так выборка только тех данных, которые потребуются, и является инициализацией модели. Пускай система показывает машины. Есть модель Car с свойством color, есть метод контроллера show($id) и list($color_filter = null). Когда в первом мы дергаем что-то вроде $car = Car::findByPK($id) — это к бизнес-логике не относится, простая инициализация, чистая логика хранения, так? Почему же когда во втором дергаем что-то типа $cars = Car::findByColor($color), то это становится бизнес-логикой — ведь та же инициализация?
                                            0
                                            Car::findByColor — это нормально. В базе, может, вообще три поля r, g, b — мы от этого абстрагированы.

                                            Я про код в контроллере условно вида

                                            Car::findAll()->where('color', '=', $color)->and('is_hidden', '=', false)->orderBy('updated')

                                            — вот такое как раз должно скрываться внутри того же DM.
                                              0
                                              Так это абстрагирование ответственности контроллера по инициализация модели от логики хранения, но к самой модели оно никакого отношения не имеет, разве что чисто случайно находится в тех же классах из-за выбранного паттерна ORM, и то не факт, если есть отдельный класс типа CarTable.
                                  0
                                  Именно по этому я написал что иногда разграничивать контроллер и вью — нецелесообразно. Но приведенный подход как минимум может сделать код реюзабельным:

                                     public function actions()
                                      {
                                          return [
                                  
                                              'view1' => [
                                                  'class' => ARViewAction::class,
                                                  'modelClass' => User::class,
                                                  'criteria' => [
                                                      'with' => [
                                                          'orders' => [
                                                              'condition' => 'sum < 1000'
                                                          ]
                                                      ]
                                                  ]
                                              ]
                                          ];
                                      }
                                  


                                  Вот пример когда вьюха подразумевает что ей передают дата-провайдер:

                                     public function actions()
                                      {
                                          return [
                                  
                                              // Если вьюха подразумевает что ей передают dataProvader'ы
                                              // в AR метода getRelationCriteria(relationName, additionalCriteria) нет, но его можно самому дописать
                                              'view2' => [
                                                  'class' => ARViewAction::class,
                                                  'modelClass' => User::class,
                                                  'dataProviders' => function(User $user){
                                                          return [
                                                              'ordersDataProvider' => [
                                                                  'modelClass' => Order::class,
                                                                  'criteria' => $user->getRelationCriteria('orders', 'sum < 1000')
                                                              ]
                                                          ];
                                                      }
                                              ],
                                          ];
                                      }
                                  
                                  –3
                                  Словно из сна вышел, прочитав вашу статью. Более того — многое из написанного кажется очевидным, что верно свидетельствует о правильности предложенных подходов.

                                  Спасибо!
                                    +4
                                    Хороший пост. Yii знаю довольно поверхностно, поэтому нюансы решений должным образом оценить не берусь, но подняты проблемы, которые существуют и в других MVC-фреймворках, причём не только на PHP, особенно с использованием паттерна Active Record. Потому было бы неплохо добавить пост в хаб PHP как минимум, и, наверное, «Проектирование и рефакторинг»
                                      +2
                                      Спасибо за хороший пост про «настоящий MVC».
                                      Совсем вскользь упомянут вопрос тестирования, который на самом деле является очень важным.
                                      1) Тестирование бизнес-логики становится на порядок проще, поскольку сильно снижается трудоемкость написания модульных тестов. Не оставляя логики в контроллерах, достаточно тестировать только модели.
                                      2) Модульное тестирование контроллеров — это очень скользкая тема, поскольку все чаще превращается в функциональное тестирование. Но опять же, оно становится намного проще, если в контроллере минимум связанной бизнес-логики.

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

                                      В статье есть пример про универсальный CRUD-контроллер. К сожалению, эта универсальность разбивается в дребезги уже при небольших различиях в требованиях к выводу данных. Начинается лапша с конфигурацией, множество реализаций или еще какие-нибудь костыли. А все ради того, чтобы не копипастить инициализацию моделей в нескольких экшнах. Отличный пример из мира Yii — CListView.

                                      В заключении, хочу сказать, что Yii — это очень мощный и гибкий инструмент. Вариантов правильного применения множество, но только опытный разработчик сможет задействовать всю мощь без ущерба себе. Поэтому, мой совет — оглядеться по сторонам и посмотреть, как устроен мир других фреймворков, поинтересоваться причинами. Не хочу холиворить, но я бы посоветовал освоить symfony2 под крылом опытного коллеги, а потом с новыми знаниями возвращаться в Yii.
                                        0
                                        Но все же во вьюхах его непосредственное присутствие создает путаницу. Я много раз видел (да чего греха таить, и сам так делал), как логику представления выносили в контроллер (какие-то специальные методы для форматирования или что-то подобное) лишь по тому-что контроллер присутствовал во всех его вьюхах.

                                        Можно пример?

                                        Еще вопрос как быть с логикой вывода во вьюхах? К примеру если есть посты — вывести, если нет — сказать что постов нет
                                          +1
                                          Еще вопрос как быть с логикой вывода во вьюхах? К примеру если есть посты — вывести, если нет — сказать что постов нет

                                          Смотрим по модели.Получаем что-то вроде:
                                          <?php /** @var Post[] $posts */ ?>
                                          <?php $posts = $user->getPosts() ?>
                                          <?php if (count($posts) > 0): ?>
                                            <ul>
                                              <?php /** @var Post $post */
                                              <?php foreach($posts as $post): ?>
                                                <li>
                                                   <h1><?php echo $post->getTitle() ?></h1>
                                                   <?php echo $post->getContent() ?>
                                                </li>
                                              <?php endforeach ?>
                                            </ul>
                                          <?php else: ?>
                                            <?= _('no posts') ?>
                                          <?php endif ?>
                                          

                                            0
                                            Спасибо, я думал что то другое будет.
                                          +1
                                          Какая замечательная статья. Который раз её перечитываю. Большое спасибо автору.

                                          Only users with full accounts can post comments. Log in, please.