Наследование CRUD операций от базового контроллера

Раньше мы рассматривали возможность повторного использования стандартных CRUD операций путём выноса их в отдельные классы (наследники класса CAction в Yii). У этого способа есть альтернатива – наследование общих действий от базового контроллера. Рассмотрим этот способ подробнее, а также попробуем найти и решить некоторые его проблемы.
Использования наследования – простой способ, но изначально (как и сами наследование) не очень гибкий.
Итак, можно переместить стандартные действия в базовый контроллер, чтобы потом наследовать эти действия от него:
class Controller extends CController { public function actionIndex(){...} public function actionAdmin(){...} public function actionCreate(){...} public function actionUpdate(){...} public function actionDelete(){...} public function actionView(){...} } class PostAdminController extends Controller {}
При таком наследовании в контроллер PostAdminController (и во все остальные) автоматически добавятся стандартные действия из родительского контроллера. Если нужно изменить какое либо действие, то его необходимо переопределить в дочернем классе PostAdminController.
Но не всем контроллерам эти действия нужны. Чтобы сделать эти действия доступными только для контроллеров панели управления можно их описать в промежуточном классе AdminController:
class Controller extends CController {} class PostController extends Controller {} class AdminController extends Controller { public function actionIndex(){...} public function actionAdmin(){...} public function actionCreate(){...} public function actionUpdate(){...} public function actionDelete(){...} public function actionView(){...} } class PostAdminController extends AdminController {}
но это не даст использовать полезные действия actionIndex и actionView в контроллерах вне админки.
В обоих случаях имеется проблема с тем, что из за статической природы наследования все действия подключаются сразу. Нельзя выбрать из них только нужные. Это приведёт к тому, что придётся переопределять все ненужные методы. Например, чтобы использовать только два метода actionAdmin и actionUpdate нам нужно эмулировать отсутствие четырёх других:
class Controller extends CController { public function actionIndex(){...} public function actionAdmin(){...} public function actionCreate(){...} public function actionUpdate(){...} public function actionDelete(){...} public function actionView(){...} } class ConfigAdminController extends Controller { public function actionIndex() { throw new CHttpException(404, 'Not found'); } public function actionCreate() { throw new CHttpException(404, 'Not found'); } public function actionDelete($id) { throw new CHttpException(404, 'Not found'); } public function actionView($id) { throw new CHttpException(404, 'Not found'); } }
Для решения этой проблемы можно вести список нужных экшенов crudActions() и осуществлять проверку в каждом действии:
class Controller extends CController { public function crudActions() { return array(); } public function actionIndex() { $this->checkCrudAction(); // ... } public function actionAdmin() { $this->checkCrudAction(); // ... } public function actionCreate() { $this->checkCrudAction(); // ... } public function actionUpdate() { $this->checkCrudAction(); // ... } public function actionDelete() { $this->checkCrudAction(); // ... } public function actionView() { $this->checkCrudAction(); // ... } protected function checkCrudAction() { if (!in_array($this->action->id, $this->crudActions()) throw new CHttpException(404, 'Not found'); } }
В дочерних контроллерах теперь можно переопределить список необходимых им методов:
class ConfigAdminController extends Controller { public function crudActions() { return array( 'admin', 'update', ); } }
Каждое действие перед своей работой вызывает проверку на вхождение себя в список указанных в методе crudActions().
Чтобы не загружать базовый контроллер лишними проверками целесообразно вынести их в отдельный фильтр:
class CrudActionFilter extends CFilter { public $actions = array( 'index', 'admin', 'create', 'update', 'delete', 'view', ); protected function preFilter($filterChain) { $controller = $filterChain->controller; $action = $controller->action; if (in_array($action->id, $this->actions) && !in_array($action->id, $controller->crudActions()) throw new CHttpException(404, 'Not Found'); return true; } }
и подключить этот фильтр к базовому контроллеру:
class Controller extends CController { public function filters() { return array( array( 'CrudActionFilter', ) ); } public function crudActions() { return array(); } public function actionIndex(){...} public function actionAdmin(){...} public function actionCreate(){...} public function actionUpdate(){...} public function actionDelete(){...} public function actionView(){...} }
Дочерние контроллеры останутся без изменений:
class ConfigAdminController extends Controller { public function crudActions() { return array( 'admin', 'update', ); } }
Это теперь стало похоже на подключение действий, оформленных в виде классов из рассмотренного ранее способа выноса действий в отдельные классы:
class ConfigAdminController extends Controller { public function actions() { return array( 'admin'=>'DAdminAction', 'update'=>'DUpdateAction', ); } }
но не поддерживает переименования действий.
Дальше можно пофантазировать и построить действия так, чтобы их можно было конфигурировать через метод crudActions(). Например так:
class ConfigAdminController extends Controller { public function crudActions() { return array( 'admin'=>array('view'=>'index', 'ajaxView'=>'_grid'), 'update', ); } }
или так:
class ConfigAdminController extends Controller { public function crudActions() { return array( array('admin', 'view'=>'index', 'ajaxView'=>'_grid'), 'update', ); } }
И дополнить базовый контроллер методом getActionParam($action, $param) для извлечения параметров из этого массива. Не забудьте при этом переделать проверки в фильтре CrudActionFilter под новую структуру.
Donna Insolita:-) Бритва Оккама.
В случае админки отдельным модулем или приложением вполне достаточно
public function actionSomething() { throw new CHttpException(404, 'Not found'); // or $this->redirect(array('otheraction')); }
Елисеев ДмитрийДостаточно, но по пять таких заглушек подряд (как в третьем листинге) в некоторых местах будут смотреться весьма странно.
Denis Klimenkoотносительно недавно пришел к такоку аля generic-контроллеру, указываем классы которые нам помогут и готово
class BucketController extends AdminRepositoryController { protected $repository = BucketRepository::class; protected $model = Bucket::class; protected $viewModel = BucketView::class; protected $entityName = 'bucket'; protected $routePrefix = 'admin.bucketmanage.buckets'; } abstract class AdminRepositoryController { /** @var AdminRepositoryContract */ protected $repository; protected $model; protected $viewModel; protected $entityName; protected $routePrefix; protected $storeRequest; protected $updateRequest; public function __construct() { $this->viewModel = App::make($this->viewModel); $this->repository = App::make($this->repository); View::share('routePrefix', $this->routePrefix); View::share('entityName', $this->entityName); } public function index(Request $request) { return View::make($this->getIndexView(), [ 'grid' => $this->viewModel->getGrid($request), 'routePrefix' => $this->routePrefix, 'entityName' => ucfirst($this->entityName) ]); } public function create() { return $this->showEditForm(new $this->model); } public function store() { $request = App::make($this->storeRequest ?? Request::class); $entity = $this->repository->createByAdmin($request); return Redirect::route($this->routePrefix . '.edit', [$entity->id]); } public function edit($id) { return $this->showEditForm($this->repository->findFirstById($id)); } public function update($id) { $request = App::make($this->updateRequest ?? Request::class); $entity = $this->repository->updateByAdmin($request, $this->repository->findFirstById($id) ); return Redirect::route($this->routePrefix . '.edit', [$entity->id]); } public function destroy($id) { $this->repository->deleteEntity($id); } protected function showEditForm($entity) { $formFields = $this->getFormFields($entity); $viewModel = $this->viewModel; return View::make($this->getEditView(), [ 'entity' => $entity, 'formFields' => !empty($formFields) ? $formFields : $viewModel::getFormFields($entity), ]); } protected function getIndexView() { return 'admin.components.genericView.index'; } protected function getEditView() { return 'admin.components.genericView.edit'; } protected function getFormFields($entity): array { return []; } }