Выносим 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.

Немного модернизируем базовые действия и вынесем в классы:

Коллекция действий на GitHub

Можно скопировать всю папку 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(); ?>
<!-- Выходим из админки на просмотр непосредственно на сайте -->
<?php $this->redirect($model->getUrl()); ?>

или

<!-- Обновляем Flash-сообщения -->
<?php $this->reflash(); ?>
<!-- Возвращаемся на форму редактирования -->
<?php $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));
        }
    }
}

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

У этого способа есть альтернативная замена – это наследование общих действий от базового контроллера.

Старайтесь тоже сокращать код. Берегите лес...

Другие статьи

Разговорились сегодня насчёт вывода списка чекбоксов в админке для выбора категорий к записи, то есть для связи MANY_MANY. Предоположим, что в нашем блоге есть записи и категории. Или товары в магазине и категории. При этом у каждой записи или у каждого товара можно выбрать несколько категорий. Как вывести этот список на странице редактирования статьи или товара?

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

При разработке интернет-магазинов или различных информеров для сайтов часто приходится реализовывать получение актуальных курсов валют. Не все разработчики знают, что достаточно удобно получать курсы на любую дату используя API сайта Центрального банка РФ.

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

Комментарии

 

Donna Insolita

Ещё один вариант - создать свой базовый контроллер с публичным свойством для передачи названия модели, описать в нём стандартные экшены, и после наследоваться от него. По необходимости в наследниках экшены можно переопределять и дополнять..

Ответить

 

Елисеев Дмитрий

О наследовании экшенов я думал, но из-за меньшей гибкости и большей сложности отказался от него. Написал об этом в в новой статье.

А создать публичное свойство с именем модели в контроллере можно и сейчас. Нужно лишь перенести методы createModel() и loadModel($id) в базовый контроллер.

Ответить

 

seobot

Супер рецепты! Полезно! Спасибо!

Ответить

 

Andrey

Спасибо за статью!

Как вариант - можно ли в DCrudAction засунуть filters и accessRules ?

Ответить

 

Andrey

В CrudModule ничего писать не надо?

Я подключил в модулях 'crud', но к примеру находясь в actionAdmin ловлю ошибку:

include(DAdminAction.php) [<a href='function.include'>function.include</a>]: failed to open stream: No such file or directory
Ответить

 

Andrey
'modules'=>array(
     ...
        'crud',
    ...
),
Ответить

 

Дмитрий Елисеев

Добавьте путь к файлам в import-секцию конфига или произведите импорт перед вашим контроллером:

Yii::import('application.modules.crud.components.*');
Ответить

 

Andrey

Это проделано, работает!

Ответить

 

Andrey

Добрый день,

Почему может вылазить ошибка:

Method CController::createModel() not found

Это из DCrudAction, я подключил модуль в конфиге и сделал импорт. Сами действия подключаю в контроллере.

Ответить

 

Дмитрий Елисеев

А в контроллер добавлен метод createModel()?

Ответить

 

Дмитрий

А зачем необходим метод "createModel()"?
Почему просто не указывать имя модели в свойстве класса, например "public $modelName;" и уже во внешнем CAction брать его с данного свойства. Просто не могу понять в чем плюс такого решения как "createModel()".

Ответить

 

Дмитрий Елисеев

Например, чтобы указать там сценарий или присвоить полям значения по умолчанию.

Ответить

 

Дмитрий

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

Ответить

 

Андрей Сучилов

Всем привет! Спасибо за рецепт. Добавил в него пару мелочей:
В DCrudAction :

	
public $pageTitle = false;
public $breadcrumbs = false;
public $menu = false;
	
protected function prepare () {
		
	if ($this->pageTitle !== false)
		$this->controller->pageTitle = $this->pageTitle;
		
	if ($this->breadcrumbs !== false)
		$this->controller->breadcrumbs = $this->breadcrumbs;
		
	if ($this->menu !== false)
		$this->controller->menu = $this->menu;
		
	return true;
}

в его наследниках, например DAdminAction:

public function run() {
	$this->prepare ();
	...
}

В контроллере :

public function actions() {
	return array(
		'index'=>array(
			'class' => 'DAdminAction',
			'pageTitle' => 'Управление пользователями',
		),
		...
	);
}
Ответить

 

Andrey

Подскажите пожалуйста, как нужно делать реализацию в модели метода published()
В классе Stock и его поведениях не найден метод или замыкание с именем "published". Имеется ввиду cdbCriteria('public = 1') ?

Ответить

 

Andrey

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

class ActiveRecord extends CActiveRecord {
...
    public function published(){
        $criteria = new CDbCriteria();
        $criteria->condition = 'EXIST = '.STATUS_PUBLIC;
        $model = $this::model(get_called_class())->findAll($criteria);
        return $model;
    }
...
}
Ответить

 

Дмитрий Елисеев

Добавьте именованную группу условий:

public function scopes()
{
    return array(
        'published'=>array(
            'condition'=>'EXIST=' . STATUS_PUBLIC,
        );
    }
}

и используйте как метод:

Stock::model()->published()->findAll()
Ответить

 

Andrey

Благодарю!

Ответить

 

Andrey

Добрый день, из за чего может вываливаться такая ошибка?

'Контроллер ConfigUserController не может найти представление "_grid" и как влияет на процесс ajaxView?

Ответить

 

Andrey
if ($this->ajaxView && Yii::app()->request->isAjaxRequest) {
    $this->controller->renderPartial($this->ajaxView, [
        'model'=>$model,
        'title'=>$modelName::TITLE
    ]);
}

Видимо тут ошибка.

Ответить

 

Дмитрий Елисеев

Значит у Вас нет представления _grid.php.

Ответить

 

Andrey

Благодарю!

Ответить

 

Grigory

На сайте есть форма заказа обратного звонка. Нужно защитить её капчей. Форма обратного звонка отображается на всём сайте и используется со всеми контроллерами текущими и будущими.
И в тогда данный совет очень помог: У этого способа есть альтернативная замена – это наследование общих действий от базового контроллера.

Спасибо!

Ответить

 

Дмитрий Елисеев

Обычно такое делается виджетом, в ActiveForm которого прописывается 'action' => ['/site/call'] и указывается captсhaAction.

Ответить

Оставить комментарий

Войти | Завести аккаунт


(никто не увидит)



Можно использовать теги <p> <ul> <li> <b> <i> <a> <pre>