Xpoint
   [напомнить пароль]

Разъясните про контроллеры в рамках MVC

Метки: [без меток]
2008-11-28 12:42:33 [обр] triumvurat[досье]

Сразу приведу пример задачи, с которой работаю.

Допустим мы разрабатываем административный интерфейс, в котором есть два типа визуальных страниц. Это:

  • Список пользователей, URL: /controller=user&action=list
  • Редактирование пользователя, URL: /controller=user&action=edit&id_user=22
  • Добавление пользователя аналогично редактированию, за исключением того, что GET-переменная id_user отсутствует, что говорит скрипту о том, что это добавляется новый элемент данных.

На странице "Список пользователей" мы можем

  • Удалять пользователей (нажатие на одну гиперссылку, 1 GET запрос). URL: /controller=user&action=delete&id_user=22
  • Переходить на страницу "Редактирование пользователя" (нажатие на одну гиперссылку, 1 GET запрос). URL: /controller=user&action=edit&id_user=22

На странице "Редактирование пользователя" присутствует форма, в которой отображена информация о пользователя в текстовых полях. При нажатии на кнопку мы посылаем POST-запрос.

В чем мой вопрос.

Вопрос I

Нужно ли разделять все части MVC два типа: user и user_list?

  • На контроллер user, который будет обрабатывать action-ы edit, delete
  • На контроллер user_list, который будет исполнять метод list (вывод списка пользователей)

До сегодняшнего дня я использовал схему разделения. Имел:

Модели: user_model, user_list_model
Представление: user_view, user_list_view
Контроллеры: user_controller, user_list_controller

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

Сейчас я всё объединил - теперь все части MVC имеют по 1 классу: класс User_Model, User_View, User_Controller.
Вот это правильно или нет?

Вопрос II

Я создал части MVC в рамках административного интерфейса. Должен ли чисто теоретически контроллер быть мегауниверсальным, что бы его использование было возможно как на frontend, так и на backend части сайта? Или всёже это разные сущности?


Абстрактный класс моего контроллера (вместо user он оперирует post, но это не суть важно):

class Admin_Post_Controller extends BaseController
{
    public function __construct(){}

    public function run()
    {
        parent::run();

        $this->model = new Post_Model();

        if ($this->request->__isset('id_post') && $this->request->id_post)
        {
            $this->model->findById($this->request->id_post);

            if (!$this->model->getId())
            {
                redirect('Поста с идентификатором '.$this->request->id_post.' не существует.');
            }
        }

        // соответствие действие => view
        // не для всех жействий нужен view, т.к. в остальных случаях все public actions 
        // после отработки выполняют header('Location: ...');
        $view_config = array('edit'=>'Blog_EditPost.html', 'list'=>'Blog_List.html');

        if (!$this->request->__isset('act'))
        {
            $this->request->act = 'list';
        }

        // Если запрошенное действие требует view объекта, мы его инстанцируем
        if (array_key_exists($this->request->act, $view_config))
        {
            $this->view = new Admin_Post_View(getConfig('admin_dir', 'templates').$view_config[$this->request->act]);
        }

        // метод родительского класса выполняет запрошенный метод
        $this->action();
    }

    // Главный метод - вывод списка постов.
    public function list()
    {
        // Получаем список записей.
        $this->model->selectList(0, 10);

        // Отдаем список объектов Post_Model Object во view.
        $this->view->makePostsList($this->model->getData());
    }

    // Редактирование поста.
    public function edit()
    {
        // если идет добавление/редактирование поста
        if (POST)
        {
            $this->process_edit();
        }

        // это сработает только тогда, когда возникла ошибка пользователя во время 
        // POST-запрпоса
        $this->view->setData($this->model->getData());
    }

    // Удаление поста, его изображений.
    public function delete()
    {
        $this->model->delete();

        $header = getConfig('params', 'notification', 'result', 'error');
        $mess = 'Пост «<strong>'.format::hsc($this->model->post_header).'</strong>» удалён.';
        redirect('Пост '.$this->model->post_header.' удалён.');
    }

    // Обработка POST-запроса.
    private function process_edit()
    {
        // валидация 
        $validator = new Post_Validator($this->model, $this->request, loadMessages('Post'));
        $validator->checkPostText('post_text');
        $validator->checkPostHeader('post_header');
        $validator->checkPostTitle('post_title', 'post_header');
        $validator->checkPostUrl('post_url', 'post_header');

        $this->model->setData($this->request->getData());

        // если возникли ошибки, отдаем их во view
        if ($err = $validator->getErrors())
        {
            $this->view->setErrorMessage($err);
        }
        // иначе сохраняем пост и location...
        else
        {
            $this->model->save();
            
            redirect('Пост сохранен');
        }
    }
}
спустя 9 минут [обр] MiRacLe(47/77)[досье]
М всего три печатных листа, не слишком ли кратко?
спустя 3 минуты [обр] triumvurat[досье]
MiRacLe[досье] Код, приведенный здесь — не рабочий, это образец. Я не принуждаю его разбирать. Вопрос мой можно и без кода понять. Код приведён в довесок.
спустя 1 час 8 минут [обр] Thirteensmay(17/157)[досье]
  1. Разумно.
  2. Желательно. ибо деление на frontend и backend в известной мере условность.
спустя 1 час 1 минуту [обр] triumvurat[досье]
Thirteensmay[досье] Я не понял насчет первого вопроса, Вы имели в виду что разумно иметь только один контроллер user или иметь user и user_list?
спустя 1 час 23 минуты [обр] Thirteensmay(17/157)[досье]
Разумно иметь только один контроллер user.
спустя 42 минуты [обр] triumvurat[досье]
Желательно. ибо деление на frontend и backend в известной мере условность.
Админская часть априори содержит в себе больше кода. Проверку на админа, права.. Не выльется ли такая мешанина в неуправляемый бардак кода?
спустя 31 минуту [обр] Thirteensmay(17/157)[досье]
Выльется или нет - зависит от разработчика, MVC тут почти не причем, админку всеравно придется как-то писать. Или вы хотите сказать что использование MVC ведет к неуправляемой мешанине бардака ? ;) Зачем же вы тогда вообще ее используете ? Типа здесь мелочь - намешаем, а вот здесь уже серьезно, значит MVC использовать нельзя, это чей подход такой ?
спустя 5 часов [обр] Igor Rulyov(0/19)[досье]

Имхо:

  1. Нет. Контроллер отвечающий за операции над конкретным типом - должен быть один. Пускай это будет 50 методов. Ничего страшного.
  1. Контроллеры раздельные.

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

  • http://server/path/controller/list/?cat=1
  • в таблице ищется категория id=1
  • категории присвоен определенный тип (тип известен после шага выше)
  • загружается сервис типа (DAO)
  • на сервисе типа вызывается общий для всех сервисов метод list()
  • т.к. типы разные по структуре - то их надо отображать разными view иклудами - это единственный if<>else в методе контроллера.

тоже самое для метода detail(), только на DAO вызывается общий для всех сервисов метод - get(id). Ну и view-инклуд соответственно другой.

P.S.: Удаление данных - только post-методом с подтверждением (JS confirm достаточно, если не планируется barrier free web).

спустя 12 часов [обр] Thirteensmay(17/157)[досье]

Igor Rulyov[досье]

  1. Контроллеры раздельные

То что раздельные для операций над конкретными типами это очевидно, но они не должны дублировать функционал фронтенда (т.е. не должно быть например user_frontend и user_backend, а только один user). Cовокупность контроллеров (user, news, и т.п.) не привязывается к frontend/backend и в известной мере должна быть "мегауниверсальной", т.е. достаточной для реализации всего функционала. В этом случае деление frontend/backend остается логическим, а не физическим, как ему и подобает быть, и определяется лишь всепронизывающим механизмом распределения доступа.

спустя 12 часов [обр] triumvurat[досье]
они не должны дублировать функционал фронтенда
Наверное тогда стоит backend делать extends frontend.
спустя 12 часов [обр] Igor Rulyov(0/19)[досье]
они не должны дублировать функционал фронтенда
Тоже вариант.
Но я делал раздельно (физически). В итоге два самостоятельных проекта, доступных под разными <path>/*.
В тоже время оба делят конфигурации, итд... все что относится к commons.
В итоге вопрос прав доступа к контроллерам админа решился в двух строках в фильтре доступа (который стартует раньше чем диспетчер контроллеров).
Если совмещать контроллеры... то я пока не задумывался как сделать реально удобную и простую систему проверки прав доступа на уровне метода.
спустя 21 час [обр] triumvurat[досье]
Igor Rulyov[досье] А Вы как сделали прав проверку? Обрисуйте, а то я пока не задумывался как это в рамках MVC сделать)
спустя 1 день 12 часов [обр] Igor Rulyov(0/19)[досье]

У меня примитивная проверка на уровне контроллера. Т.е. либо у пользователя есть права на контроллер либо их нет.

class Account
{
 public function hasRight($r)
 {
  return (!is_null($this->rights))&&(($this->rights&$r)===$r);
 }
}
abstract class AbstractController {

 final public function _execute(&$rreq, $action)
 {
  if (is_null($action) || strlen((string)$action)==0) {
   trigger_error('[' . __CLASS__ . '] Method name must be not empty.', E_USER_ERROR);
   exit(0);
  } else {
   $flowNext = FALSE;
   if (isset($this->_accessRight) && ThreadRegistry::isSet('account')) {
    $account = ThreadRegistry::get('account');
    $flowNext = $account->hasRight($this->_accessRight);
   } else {
    $flowNext = TRUE;
   }
   if ($flowNext===TRUE) {
    $action = (string)$action . 'Method';
    if (method_exists($this, $action)) {
     $this->$action();
    } else {
     trigger_error('[' . __CLASS__ . '] Not defined method: <' . $action . '> in <' . $this->_getClassName() . '>.', E_USER_ERROR);
     $this->noSuchMethod();
    }
   } else {
    trigger_error('[' . __CLASS__ . '] No access-permissions to <' . $this->_getClassName() . '>.', E_USER_ERROR);
    $this->noRightsMethod();
   }
  }
 }

 protected function noRightsMethod() {...}
 protected function noSuchMethod() {...}

}
class ConcreteController extends AbstractController {
 protected $_accessRight = 1;
 ...
}

Примерный workflow:

  • begin
  • dbfilter
  • authfilter
    • logon
    • ThreadRegistry::set('account', $Account)
    • logoff
    • etc
  • dispatcher
    • Парсинг URI
    • если пользовательской сессии не существует: LoginController->showloginformMethod()
    • или вызов метода _execute() вызванного контроллера

Далеко не самое гибкое решение в некоторых планах, но...

Оговорка в методе _execute():

isset($this->_accessRight) && ThreadRegistry::isSet('account')

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

Права (побитовые операции... тут все понятно):... например
1+2+4=7
(7&1)===1) true
(7&8)===8) false

т.е. я просто присвоил каждому контроллеру число... а нужная сумма закреплена за пользователем (класс Account).

Потом как-нибудь надо будет переписывать... пока нет идей.

спустя 10 часов [обр] triumvurat[досье]

Igor Rulyov[досье] В коде большом тяжело разобраться.. я попробую.

Я так понимаю, у вас права на каждый action хранятся для каждого пользователя/группы?

спустя 1 день 4 часа [обр] Igor Rulyov(0/19)[досье]

triumvurat[досье]

Да.
Только не на экшн, а не контроллер. Если у пользователся есть права на конкретный контроллер - то ему "видимы" все экшены.

Powered by POEM™ Engine Copyright © 2002-2005