Выносим CRUD действия контроллеров в классы в Yii
Любой программист с опытом осознаёт, что в неудачно спроектированном тяжёлом проекте изначально или со временем накапливается много неуправляемого и ненужного мусора. Это, например, повторяющийся код. В описании контроллера официального руководства Yii Framework указано, что Yii поддерживает вынос действий в отдельные классы и описывается процедура подключения этих действий к контроллерам. Но мало кто пользуется этим способом, так как не находит действительно тяжёлых повторяющихся экшенов.
Попробуем применить этот приём к тому что есть, чтобы по возможности сократить наш код.
С помощью модуля Gii можно автоматически генерировать каркас контроллера с действиями для просмотра, создания, редактирования и удаления любых сущностей (так называемых CRUD-операций).
Для управления записями блога получаемый контроллер имеет следующую структуру (фильтр доступа рассматривать не будем, так как для понимания смысла он не нужен):
class PostController extends Controller { // ... public function actionView($id){ // .. } public function actionCreate(){ // ... } public function actionUpdate($id){ // ... } public function actionDelete($id){ // ... } public function actionIndex(){ // ... } public function actionAdmin(){ // ... } public function loadModel($id){ // ... } protected function performAjaxValidation($model){ // ... } }
Как видим, здесь собраны базовые действия
actionView($id)
;actionCreate()
;actionUpdate($id)
;actionDelete($id)
;actionIndex()
;actionAdmin()
и вспомогательные методы
loadModel($id)
;performAjaxValidation($model)
.
Код этих действий максимально прост, но при умелом разделении обязанностей всех компонентов (а именно перенесение логики из контроллера в толстые модели, отдельные фильтры, формы, правила валидации) они практически без изменения могут покрыть почти все требования по работе с различными сущностями сайта.
Если на публичной стороне вывод в разных модулях порой значительно различается, то в админке обычно всё стандартно. При наличии на сайте нескольких десятков типовых сущностей образуется соразмерное количество однотипных контроллеров с одинаковыми методами. Гораздо экономичнее вместо копирования одних и тех же экшенов вынести общие стандартные действия в отдельные классы и по необходимости подключать к контроллерам.
К базовым действиям полезно добавить ещё actionToggle($id, $attribute)
для переключения некоторых флагов модели, которое можно использовать для работы с DToggleColumn.
Немного модернизируем базовые действия и вынесем в классы:
Можно скопировать всю папку crud
в директорию protected/modules
и использовать как модуль. Для этого нужно будет подключить данный модуль в конфигурационном файле protected/config.main.php
'modules'=>array( // ... 'crud', )),
Также можно взять только файлы действий из папки crud/components
и поместить их в protected/components/crud
. Для работы переводов сообщений необходимо содержимое папки crud/messages
поместить в общую папку приложения protected/messages
, а в коде все вызовы
Yii::t('CrudModule.crud', ...)
вручную заменить на
Yii::t('crud', ...)
Вдальнейшем будут различаться только пути для импортирования классов:
// при использовании как модуль Yii::import('crud.components.*'); // при использовании без модуля Yii::import('application.components.crud.*');
Пример использования
В качестве примера создадим простейший контроллер для управления записями блога с использованием этих действий:
Yii::import('crud.components.*'); class PostController extends Controller { public function actions() { return array( 'index'=>'DIndexAction', 'admin'=>'DAdminAction', 'create'=>'DCreateAction', 'update'=>'DUpdateAction', 'toggle'=>'DToggleAction', 'delete'=>'DDeleteAction', 'view'=>'DViewAction', ); } public function getIndexProviderModel() { return Post::model()->published(); } public function createModel() { $model = new Post; $model->date = date('Y-m-d H:i:s'); return $model; } public function loadModel($id) { $model = Post::model()->findByPk($id); if($model === null) throw new CHttpException(404, 'Страница не найдена'); return $model; } public function performAjaxValidation($model){ if(isset($_POST['ajax']) && $_POST['ajax']==='blog-post-form'){ echo CActiveForm::validate($model); Yii::app()->end(); } } }
При работе этим действиям необходимо обеспечить поставку нужных моделей. Для этого в контроллере должны быть определены соответствующие методы:
// Delete, Toggle, Update, View public function loadModel($id){...} // Admin, Create public function createModel(){...} // Index public function getIndexProviderModel(){...}
Для работы Ajax валидации добавьте стандартный метод performAjaxValidation
и объявите его публичным
// Create, Update public function performAjaxValidation($model) { if(isset($_POST['ajax']) && $_POST['ajax']==='blog-post-form'){ echo CActiveForm::validate($model); Yii::app()->end(); } }
Он подхватится действиями Create и Update автоматически.
Также для контроля операций действия запрашивают у контроллера некоторые методы обратного вызова:
// Create public function beforeCreate($model){...} // Update public function beforeUpdate($model){...} // Toggle public function beforeToggle($model){...} // Delete public function beforeDelete($model){...}
Использовать их необязательно. Все методы должны быть объявлены публичными. В них можно производить, например, присвоение необходимых значений атрибутам модели или проверку доступа.
Следующий пример показывает, как прервать операцию когда пользователь пытается удалить чужую запись:
Yii::import('crud.components.*'); class PostController extends Controller { public function actions() { return array( 'index'=>'DIndexAction', 'admin'=>'DAdminAction', 'create'=>'DCreateAction', 'update'=>'DUpdateAction', 'delete'=>'DDeleteAction', 'view'=>'DViewAction', ); } public function beforeDelete($model) { if (!$this->checkAccess($model)) throw new CHttpException(403, 'Вы не можете удалить данную запись'); } protected function checkAccess($model) { $isAuthor = $model->author_id == Yii::app()->user->id; $isAdmin = Yii::app()->user->checkAccess(User::ROLE_ADMIN); return $isAuthor || $isAdmin; } // ... }
Метод beforeDelete
также пригодится для проверок вроде «В данной категории есть товары. Удалите или переместите их в другие категории» и подобных.
Каждое действие имеет свои настройки. Их можно подсмотреть в исходном коде классов.
Конфигурируются действия стандартным образом:
Yii::import('crud.components.*'); class PostAdminController extends Controller { public function actions() { return array( // в админке используем по умолчанию actionAdmin вместо actionIndex // и задаём отдельное представление для оптимизации Ajax обновления грида 'index'=>array( 'class'=>'DAdminAction', 'view'=>'index', 'ajaxView'=>'_grid' ), 'update'=>'DUpdateAction', 'toggle'=>array( 'class'=>'DToggleAction', 'attributes'=>array('public', 'popular') ), 'delete'=>'DDeleteAction', // Разрешаем получение данных по JSON при наличии $_GET['json'] 'view'=>array( 'class'=>'DViewAction', 'json'=>true ) ); } // ... }
Если какое либо из действий вам не нужно, то просто не подключайте его. Если стандартное не подходит, то просто напишите вместо него своё. По крайней мере, действие удаления в стандартном виде пригодится почти во всех контроллерах.
При успешном сохранении модели действия Create, Toggle и Update вместо обновления формы вызовом CController::refresh()
производят перенаправление на действие View. Если просмотр в админке вам не нужен (например, нужно после сохранения вернуться на форму), то создайте фиктивное представление view.php с повтором сообщения и с нужным редиректом вроде
<!-- Обновляем Flash-сообщения --> <?php $this->reflash(); <!-- Выходим из админки на просмотр непосредственно на сайте --> $this->redirect($model->getUrl());
или
<!-- Обновляем Flash-сообщения --> <?php $this->reflash(); <!-- Возвращаемся на форму редактирования --> $this->redirect('update', array('id'=>$model->id));
Метод reflash()
просто восстанавливает все мгновенные сообщения. Его можно поместить в базовый контроллер:
class Controller extends CController { public function reflash() { foreach (array('success', 'error', 'notice') as $type){ if(Yii::app()->user->hasFlash($type)) Yii::app()->user->setFlash($type, Yii::app()->user->getFlash($type)); } } }
Таким образом, мы избавились от необходимости повторять стандартные методы во всех контроллерах, чем изрядно сэкономили бумагу килобайты.
У этого способа есть альтернативная замена – это наследование общих действий от базового контроллера.
Старайтесь тоже сокращать код. Берегите лес...
Ещё один вариант - создать свой базовый контроллер с публичным свойством для передачи названия модели, описать в нём стандартные экшены, и после наследоваться от него. По необходимости в наследниках экшены можно переопределять и дополнять..
О наследовании экшенов я думал, но из-за меньшей гибкости и большей сложности отказался от него. Написал об этом в в новой статье.
А создать публичное свойство с именем модели в контроллере можно и сейчас. Нужно лишь перенести методы createModel() и loadModel($id) в базовый контроллер.
Супер рецепты! Полезно! Спасибо!
Спасибо за статью!
Как вариант - можно ли в DCrudAction засунуть filters и accessRules ?
В CrudModule ничего писать не надо?
Я подключил в модулях 'crud', но к примеру находясь в actionAdmin ловлю ошибку:
Добавьте путь к файлам в import-секцию конфига или произведите импорт перед вашим контроллером:
Это проделано, работает!
Добрый день,
Почему может вылазить ошибка:
Method CController::createModel() not found
Это из DCrudAction, я подключил модуль в конфиге и сделал импорт. Сами действия подключаю в контроллере.
А в контроллер добавлен метод createModel()?
А зачем необходим метод "createModel()"?
Почему просто не указывать имя модели в свойстве класса, например "public $modelName;" и уже во внешнем CAction брать его с данного свойства. Просто не могу понять в чем плюс такого решения как "createModel()".
Например, чтобы указать там сценарий или присвоить полям значения по умолчанию.
Ну касательно полей, я думаю что это лучше делать в модели - например в beforeSave. А касательно сценария - его можно указывать в свойствах класса по умолчанию, и если нужно у нас будет возможность задать сценарий при подключении самого action, так как это реализовано у вас в AdminAction.
Всем привет! Спасибо за рецепт. Добавил в него пару мелочей:
В DCrudAction :
в его наследниках, например DAdminAction:
В контроллере :
Подскажите пожалуйста, как нужно делать реализацию в модели метода published()
В классе Stock и его поведениях не найден метод или замыкание с именем "published". Имеется ввиду cdbCriteria('public = 1') ?
Как правильно можно исправить такую запись в базовой модели?
Добавьте именованную группу условий:
и используйте как метод:
Благодарю!
Добрый день, из за чего может вываливаться такая ошибка?
'Контроллер ConfigUserController не может найти представление "_grid" и как влияет на процесс ajaxView?
Видимо тут ошибка.
Значит у Вас нет представления _grid.php.
Благодарю!
На сайте есть форма заказа обратного звонка. Нужно защитить её капчей. Форма обратного звонка отображается на всём сайте и используется со всеми контроллерами текущими и будущими.
И в тогда данный совет очень помог: У этого способа есть альтернативная замена – это наследование общих действий от базового контроллера.
Спасибо!
Обычно такое делается виджетом, в ActiveForm которого прописывается 'action' => ['/site/call'] и указывается captсhaAction.