Статьи

Другой подход MVC

Поэтому, прочитав и услышав много о шаблоне Модель / Представление / Контроллер, я решил применить его к новому веб-приложению в офисе. После долгих проб и ошибок, редизайна, рефакторинга и большего количества чтения я придумал простую архитектуру, которая на самом деле работает довольно хорошо. Если вы совсем не знакомы с MVC, я предлагаю вам прочитать вики-страницу для краткого введения. Готовы?

Мое приложение может быть разбито примерно на 5 объектов: объект FrontOffice, контроллеры, действия, ActionResults и представления.

FrontOffice
FrontOffice — это место, где поступает запрос. Поскольку запрос может поступать через любой протокол, я определил FrontOffice как абстрактный класс, расширяющий его для каждого протокола. В этом случае мы будем использовать объект FrontOffice_URL, чтобы извлечь запрос из URL. FrontOffice просто переводит, передает запрос контроллеру, который отвечает за обработку запрошенного действия. Это называется единой точкой входа.

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

Действие
Объекты действия имеют дело с авторизацией действия (бизнес-правилами) и выполнением действия. Это на самом деле реализация командного шаблона. В зависимости от результата метода Action :: execute () , Action возвращает объект ActionResult. Это определяет маршрутизацию приложения. Например: если запись успешно добавлена ​​в БД, перенаправьте клиента на страницу обзора. В противном случае снова откройте страницу «Добавить запись» и отобразите сообщение об ошибке.

ActionResults
Объекты ActionResult возвращаются объектом Action в контроллер. Они реализуют простой интерфейс, который просто говорит, что каждый объект ActionResult должен иметь метод execute (). Примерами объектов ActionResult являются ActionResult_View (присутствует HTML), ActionResult_Redirect (клиент перенаправления) и ActionResult_Download (представляет загрузку файла).

Представления
Представления — это простые php-файлы, которые определяют html-код страницы и объединяют некоторые простые данные.

На диаграмме ниже показано, как система обрабатывает запрос.

Модель
Итак, увидев это, я слышу, как вы говорите: где модель? Хороший. Это может быть сюрпризом, но в моей модели нет классов Model. На мой взгляд, модель должна обрабатывать основные правила проверки и санитарии ваших данных. Для этого я использую мощный механизм БД, написанный моим другом Арнольдом Дэниелсом. Он называется QDB и доступен бесплатно на его сайте., Для каждой из моих таблиц базы данных у меня есть файл конфигурации .ini (вы можете использовать yaml или что вы предпочитаете), который определяет тип данных и правила проверки (требуется да / нет, максимальная длина, проверка электронной почты и т. Д.). Поэтому, когда мне нужна модель в моем контроллере, я просто вызываю библиотеку QDB для записи / таблицы / набора записей, и она дает мне данные. Вам не нужно беспокоиться о заявлениях mysql или о чем-то другом, об этом все позаботятся. В общем, контроллер не знает тип базы данных, на которой вы работаете. Посмотрите примеры кода для простоты QDB.

Теперь давайте посмотрим на простой пример.


Пример
Начнем с простого примера приложения MVC, основанного на этой архитектуре. Допустим, наше приложение содержит таблицу с сотрудниками, и мы хотим обновить запись (с id = 3) в этой таблице. Я использую mod_rewrite в Apache для чистых URL. Фактический URL-адрес для этого действия будет http://mywebsite.com/index.php?__controller=Employee&__action=update&id=3, но он переписан на http://mywebsite.com/Employee/update?id=3 .

 # initialize the FrontOffice 
$fo = FrontOffice::factory('URL');

# process the request
$fo->dispatch();

Это может быть весь код PHP в корне вашего приложения. На самом деле, это мой index.php (безопасный код инициализации и аутентификация пользователя, например include ‘config.inc’; ). Похоже, волшебство, не так ли?

Так что же происходит?
Сначала мы инициализируем FrontOffice. Я использую фабричный метод, чтобы сделать это, и это возвращает объект FrontOffice_URL . Вот конструктор класса FrontOffice_URL :

class FrontOffice_URL extends FrontOffice { 

public $output = "Html";

/**
* Initialize the FrontOffice - Read from URL the controller and the action
*
* @return FrontOffice object
*/
public function __construct() {

$this->_requestVars = new Data();

# capture get
if ($_GET['__controller']) $this->_controller = urldecode($_GET['__controller']);
if ($_GET['__action']) $this->_action = urldecode($_GET['__action']);

foreach ($_GET as $k=>$v) {
$k = urldecode($k);
$this->_requestVars->{$k} = urldecode($v);
}

# capture post
foreach ($_POST as $k=>$v) $this->_requestVars->{$k} = $v;

}

}

Довольно просто, верно? Контроллер и действие извлекаются из URL, а остальные переменные запроса хранятся в общем объекте Data, который является просто классом void (в PHP вы можете устанавливать свойства на лету). Обратите внимание, что формат вывода установлен на «HTML». В будущем вы можете захотеть, чтобы ваше приложение отвечало в формате XML. В зависимости от настройки $ output, вы можете включить правильные представления. В конце статьи это показано в структуре каталогов.


Итак, мы переходим ко второму утверждению:

# load controller 
$cclass = self::getControllerClass($this->_controller);
if (!class_exists($cclass)) throw new Controller_Unknown_Exception("Unknown Controller object `".$this->_controller."` requested");

$controller = new $cclass($this);

# controller must extend Controller class
if (!($controller instanceof Controller)) throw new Controller_Illegal_Exception("Controller `$cclass` is not a valid Controller object");

# execute the action
return $controller->execute($this->_action, $this->_requestVars);

Теперь мы переместили запрос из FrontOffice в объект Controller. Вы не взволнованы? Отсюда мы на самом деле собираемся что-то сделать 🙂

Контроллером в этом случае является контроллер Employee. Мы сосредоточимся на вызываемом методе execute (). Как я уже сказал, он выполняет простое сопоставление имен действий с классами действий. Я предпочитаю не делать это прямо из URL по нескольким причинам. Метод execute () ищет и загружает класс для запрошенного Action, а затем вызывает метод Action :: execute (). Этот метод в свою очередь возвращает объект ActionResult. ActionResult :: execute () делает все, что требуется для действия (представляет представление, предлагает загрузить файл, перенаправляет клиента и т. Д.).

class Controller_Employee extends Controller { 

public function execute($action, $data)
{
$data->mode = $action;

switch (strtolower($action))
{
case "list":
$action = new Action_Overview('Employee', $data);
break;
case "update":
case "show":
case "add":
$action = new Action_Detail('Employee', $data);
break;
case "delete":
$action = new Action_Delete('Employee', $data);
break;
default :
$action = new Action_Overview('Employee', $data);
break;
}
$result = $action->execute();
if (!$result instanceof ActionResult) throw new ActionResult_Illegal_Exception('$result is not a valid ActionResult');

return $result->execute();

}

}

Это код для метода execute ().

В конструкторе Action_Detail мы имеем:

public function __construct($table, $data) 
{
if (!$data->id && $data->mode !== 'add') throw new Action_Arguments_Exception('Param id required, not given');
$this->data = $data;
$this->record = QDB::i()->table($table)->load($data->id);

if (!$this->record) throw new Action_Exception('Record not found');
}

Вы можете понять мою благодарность за библиотеку QDB, увидев это. Он полностью лишен операторов MySQL и имеет очень интуитивно понятный API.

Вернуться к Controller_Employee :: execute (). Внутри $ result = $ action-> execute () происходят две вещи .

  1. Вызывается метод действия () auth (), который проверяет такие вещи, как: если текущий пользователь не является начальником сотрудника, пользователь не может изменить запись сотрудника . В этом примере я ничего не проверю, поэтому я могу просто использовать универсальный объект Action_Detail (его метод auth () всегда возвращает true). В противном случае вы бы создали объект Action_Detail_Employee_Update, расширяет Action_Detail и переопределяете метод auth ().
  2. Если Action :: auth () возвращает true, мы можем продолжить выполнение действия.

Теперь, когда мы уполномочены обновить этого сотрудника, мы хотим перейти к той части, где мы создаем форму. Для этого я использую проприетарную библиотеку форм, которая сравнивается с PEAR HTML_QuickForm (но на самом деле лучше :)). Так что, ради аргумента, не пытайтесь понять объект $ form. Код для фактического выполнения действия выглядит примерно так:
в Action_Detail :: execute ()

$form = HTML_Form::createFromData('form', $this->record); 

# if the form is submitted and validated, redirect to overview
if ($form->isSubmitted() && $form->validate()) {
$this->record->save(); // record updaten
return new ActionResult_Redirect('/'.FrontOffice::currentController().'/list');
}

# create form
if ($this->data->mode === 'show')
return new ActionResult_View('View/Show.php', $this->record);
else
return new ActionResult_View('View/Update.php', $this->record);

ActionResult возвращается в контроллер, где мы видели return $ result-> execute (); код. В этом случае мы получаем объект ActionResult_View, возвращенный нам, с представлением и некоторыми данными для представления в представлении.

Почти там, только ActionResult_View :: execute (), чтобы обернуть это.

function execute()  
{
# Make $data available as a global in the View
$data = $this->data;

# determine output format (HTML/XML etc)
$type = ucfirst(strtolower(FrontOffice::i()->output));

# locate View file
$view = preg_replace('/View/', 'View/'.$type, $this->view);

try {
$success = @include($view);
if (!$success) throw new View_Exception("View does not exist");
} catch (View_Exception $e) {
print $e->getMessage();
}
}

Включенный сюда файл View может выглядеть примерно так:

# initialize stuff like menu/layout etc 
initLayout();

if ($data && $data instanceof QDB_Record)
{
$form = HTML_Form::createFromData('form', $data);
$form->validate();

$form->display();
}

Вот и все. Мы включаем файл PHP, и он выводит некоторый HTML для клиента.

Чтобы закрыть, вот структура каталогов, которую я использую:

Действие
  \ Delete.php
  \ detail.php
  \ Overview.php
  \ Static.php
ActionResult
  \ download.php
  \ NotAllowed.php
  \ redirect.php
  \ view.php
MyApplication
  \ Action
  \ Служащий
     \ update.php
  \ Controller
     \ Employee.php
     \ …
  \ View
     \ …

Посмотреть
  \ Html
     \ Overview.php
     \ Start.php
     \ …
   \ Xml
     \ Overview.php
     \ Start.php
     \ …

action.php
ActionResult.php
controller.php
Data.php
FrontOffice.php

Вот и все. В настоящее время мы используем эту архитектуру в большом приложении, и она оказывается очень гибкой и простой в использовании. Может потребоваться некоторое время, чтобы привыкнуть к структуре, но высокая возможность повторного использования и небольшие фрагменты кода действительно окупаются в долгосрочной перспективе.

Обратите внимание, что я упростил некоторые вещи, и я не вдавался в подробности о некоторых интерфейсах / абстрактных классах, которые я использую. Я намеревался донести свою идею и с нетерпением жду любых вопросов / предложений и комментариев.