При постановке цели всегда следует подумать, что действительно нужно. В случает с backend-частью сайта это:
- Минимум кода и как следствие некоторое обобщение данных
- Возможность расширения для особенных разделов
- Отсутствие прямой связи frontend и backend частей сайта
Минимум кода
Думаю многие разработчики, которые генерировали CRUD с помощь встроенного генератора Yii, знают, что создается практически одинаковый код, что раздражает и немного противоречит принципам ООП. Дополнительно к этому можно сказать, что действия Create и Update различаются только наличием id (у новой записи id ещё не создан), поэтому можем исключить действие Create.
Некоторое обобщение данных
В данный момент единственное обобщение данных, которое я использую — это статус “В корзине” для записи в таблице. Если рассказать об этом очень кратко, то получится, что почти во всех таблицах у меня есть поле status (tinyint, index) со значениями 0 — обычное и 1 — удаленное состояние.
Возможность расширения для особенных разделов
Наверно во всех правилах есть исключения и их мы должны обязательно предусмотреть их решение. В случае с backend это возможность переопределить любое стандарное поведение базового функционала.
Отсутствие прямой связи frontend с backend
Вackend является дополнительным функционалом и должен безболезненно удалятся из приложения. В моём случае это безболезненное удаление одноименного модуля.
Практика в 7 шагов
1. Создаем модуль «backend» стандартными средствами Gii (я не буду это подробно описывать, для этого есть официальная документация).
2. Создаем базовую модель, которая в умеет случае умеет работать с корзиной.
abstract class TModel extends CActiveRecord { const STATUS_DEFAULT = 0; const STATUS_REMOVED = 1; public function defaultScope() { return array( 'condition' => 'status=' . self::STATUS_DEFAULT ); } public function removed() { $this->resetScope()->getDbCriteria()->mergeWith(array( 'condition' => 'status=' . self::STATUS_REMOVED )); return $this; } public function restore() { if($this->getIsNewRecord()) throw new CDbException(Yii::t('yii','The active record cannot be deleted because it is new.')); if($this->status != self::STATUS_REMOVED) return false; $this->status = self::STATUS_DEFAULT; $this->save(false, array('status')); return true; } public function beforeDelete() { if($this->status == self::STATUS_DEFAULT) { $this->status = self::STATUS_REMOVED; $this->save(false, array('status')); return false; } return parent::beforeDelete(); } }
3. Создаем базовый контроллер для CRUD.
class BackendController extends CController { public $defaultAction = 'list'; public function actions() { return array( 'list' => 'backend.actions.ListAction', 'update' => 'backend.actions.UpdateAction', 'delete' => 'backend.actions.DeleteAction', 'restore' => 'backend.actions.RestoreAction', ); } }
4. Создаем базовое действие, которое «догадывается» с какой моделью и представлением работает (обычно не комментирую код, но тут специально для статьи сделал несколько комментариев).
abstract class BackendAction extends CAction { private $_modelName; private $_view; /** * Упрощенная переадресация по действиям контроллера * По-умолчанию переходим на основное действие контроллера */ public function redirect($actionId = null) { if($actionId === null) $actionId = $this->controller->defaultAction; $this->controller->redirect(array($actionId)); } /** * Рендер представление. * По-умолчанию рендерим одноименное представление */ public function render($data, $return = false) { if($this->_view === null) $this->_view = $this->id; return $this->controller->render($this->_view, $data, $return); } /** * Возвращаем новую модель или пытаемся найти ранее * созданную запись, если известен id */ public function getModel($scenario = 'insert') { if(($id = Yii::app()->request->getParam('id')) === null) $model = new $this->modelName($scenario); else if(($model = CActiveRecord::model($this->modelName)->resetScope()->findByPk($id)) === null) throw new CHttpException(404, Yii::t('base', 'The specified record cannot be found.')); return $model; } /** * Возвращает имя модели, с которой работает контроллер * По-умолчанию имя модели совпадает с именем контроллера */ public function getModelName() { if($this->_modelName === null) $this->_modelName = ucfirst($this->controller->id); return $this->_modelName; } public function setView($value) { $this->_view = $value; } public function setModelName($value) { $this->_modelName = $value; } }
5. Создаем базовое действие List.
class ListAction extends BackendAction { public function run($showRemoved = null) { $model = $this->getModel('search'); if($showRemoved !== null) $model->removed(); if(isset($_GET[$this->modelName])) $model->attributes = $_GET[$this->modelName]; $this->render(array( 'model' => $model, 'showRemoved' => $showRemoved, )); } }
6. Создаем базовое действие Update.
class UpdateAction extends BackendAction { public function run() { $model = $this->getModel(); if(isset($_POST[$this->modelName])) { $model->attributes = $_POST[$this->modelName]; if($model->save()) $this->redirect(); } $this->render(array('model' => $model)); } }
6. Создаем базовое действие Delete.
class DeleteAction extends BackendAction { public function run() { $this->getModel()->delete(); $this->redirect(); } }
7. Создаем базовое действие Restore(восстановление).
class RestoreAction extends BackendAction { public function run() { $this->getModel()->restore(); $this->redirect(); } }
Проверка
Для проверки всего вышенаписанного создадим модель и контроллер Article.
class Article extends TModel { public static function model($className=__CLASS__) { return parent::model($className); } public function tableName() { return 'article'; } public function rules() { return array( array('name, content, createTime', 'required'), ); } } class ArticleController extends BackendController { //Сейчас все довольно стандартно, поэтому ничего более не пишем }
Итог
В простейшем варианте получается, что для создание нового CRUD нужно создать пустой контроллер и два представления (list, update). Если нужно дополнительный функционал просто его дописываем или переписываем текущий.
Примечание
Я не буду описывать код представлений, ибо он скорей всего будет уникален для каждого разработчика, да и его можно взять из стандартного CRUD.

