Практическое MVC (Rating: +5)

Print RSS
Введение
Данная статья рассчитана на программистов, имеющих общие знания в области PHP, его ОО модели и практикующих модульную архитектуру. Я постараюсь не углубляться в особенности тех или иных элементов ООП, чтобы не смешивать теорию с практикой, а постараюсь описать только практические части реализации классической MVC архитектуры и некоторых ее доработок.

Совсем немного теории
MVC архитектура является подмножеством более общей модели, называемой "Архитектурой с расслоением". Данная архитектура регламентирует создание таких систем, каждая функционально-завершенная область, которых будет не просто взаимодействовать с другими, но и образовывать отдельный модуль (или слой) со своим уровнем абстракции. В общем смысле это означает, что система расслаивается на следующие основные компоненты (в порядке обобщения):
- Слой источника данных - данный слой включает механизмы непосредственного взаимодействия с наиболее нижними уровнями системы, на пример с файловой системой, базой данных и т.д. В данный слой может входить множество небольших классов, каждый из которых решает строго определенные задачи;
- Слой служб (сервисов) - данный слой включает множество классов из шаблона "Фассад", которые предоставляют более абстрактный доступ к слою источника данных, на пример это службы взаимодействия с БД. В отличии от слоя источника данных, данная служба практически не содержит логики взаимодействия с БД, а только предоставляет более удобный интерфейс для этого взаимодействия, а так же позволяет легко изменять классы слоя испточника данных без необходимости переписывания кода в остальных слоях;
- Модель домена - данный слой содержит классы, реализующие логику самой программы. Если речь идет о гостевой книге, то данный слой будет содержать классы, представляющие предметную область, API системы и логику взаимодействия с хранилищами (сессии, БД и т.д.);
- Пользовательский интерфейс - данный слой содержит элементы взаимодействия с пользователем (GUI, консоль и т.д.).
Архитектура MVC затрагивает два последних слоя: Модель домена и Пользовательский интерфейс - позволяя разделить эти два слоя через добавление еще одного компонента - Контроллера.
В общем смысле, MVC использует модульную структуру, так как регламентирует создание множества триад - Модель, Вид, Контроллер. Для примера, гостевая книга с использованием MVC может выглядеть следующим образом:
Модель
<?php
interface Message{
  function __construct($autor, $content);
  public function edit($newContent);
  public function remove();
}

interface Book{
  // Возвращает объект класса Message по его id
  public function getMessage($idMessage);
  // Возвращает объекты класса Message по их id
  public function getMessagesFromIds(array $idsMessages);
  // Возвращает объекты класса Message из временного интервала
  public function getMessagesFromInterval(\DateInterval $interval);
  // Добавляет новое сообщение в БД
  public function createMessage(Message $message);
}
Модель будет использовать сервисы для записи сообщений в БД, их чтение и редактирования.
Вид
<?php
interface Tape{
  public function printMessage(Message $message);
}
Как правило, вид использует сервисы шаблонизации и шаблон "Издатель/подписчик".
Контроллер
<?php
class ControllerBook{
  // Функция, обрабатывающая все данные от вида (пример)
  public static function main(){
    $message = $_REQUEST;
    switch($message['Active']){
      case 'CreateMessage':
        // Никакой логики, только передача управления модели!
        $newMess = new Message($message['Autor'], $message['Content']);
        $book = new Book;
        $book->createMessage($newMessage);
      break;
      case 'DeleteMessage':
	// Никакой логики, только передача управления модели!
	$book = new Book;
        $book->getMessage($message['Id'])->remove();
      break;
      case 'EditMessage':
	// Никакой логики, только передача управления модели!
	$book = new Book;
        $book->getMessage($message['Id'])->edit($message['Content']);
      break;
    }
  } 
}
// При выполнении этого скрипта, управление передается в метод main контроллера, потому этот скрипт отлично подходит для обработки всех GET и POST запросов от клиента
// Его задачей является направить эти запросы в модель
ControllerBook::main();

Как видно, модель MVC всего лишь добавляет дополнительный контроллер, который принимает GET и POST запросы от клиента (в данном случае) и перенаправляет их нужным объектам модели.
Небольшие дополнения
Я решил пойти дальше и дополнить мою систему следующими механизмами:
- Единая точка входа - все GET и POST запросы всех видов передаются в одиный контроллер, который передает их ответственному контроллеру, а тот, в свою очередь, модели. Этот подход позволяет вынести некоторую общую логику приема данных (на пример фильтрацию входящих данных) в один класс, а не дублировать ее в каждом контроллере;
- Роутер модулей - для автоматизации поиска нужного контроллера, я использовал роутер, который позволит быстро находить требуемый контроллер;
- DataMapper с архитектурой "Отдельная таблица для каждого листа" - сложный механизм, позволяющий записывать объекты в реляционный БД с учетом наследования.
Все эти дополнения будут описаны далее, но они не являются частью классического MVC, пример которого был приведен выше.
Инструменты
Прежде чем описывать реализацию системы с использованием MVC, приведу несколько интерфейсов, необходимых для работы системы. Данные инструменты относятся к слою служб, так как доступны всем модулям модели домена, с другой стороны модули модели домена доступны только своим пользовательским интерфейсам и могут взаимодействовать только через заранее определенный механизм.
Роутер
Роутер позволяет Единой точки входа находить нужный для данного запроса контроллер. Роутер имеет следующий интерфейс:
<?php
/**
 * Интерфейс управления роутингом модулей
 */
interface ModulesRouterInterface{
  /**
   * Метод возвращает контроллер для данного модуля
   * @abstract
   * @param string $moduleName Имя модуля
   * @return mixed
   */
  public function getController($moduleName);

  /**
   * Метод определяет, определен ли данный модуль в системе
   * @abstract
   * @param string $moduleName Имя модуля
   * @return boolean true - если модуль установлен, иначе - false
   */
  public function isModuleExists($moduleName);

  /**
   * Метод добавляет новой путь в роутер
   * @abstract
   * @param string $moduleName Имя модуля
   * @param \PPHP\tools\patterns\metadata\reflection\ReflectionClass $controller Отображение класса контроллера для данного модуля
   */
  public function setController($moduleName, \PPHP\tools\patterns\metadata\reflection\ReflectionClass $controller);

  /**
   * Метод удаляет данные модуля из роутера
   * @abstract
   * @param string $moduleName Имя модуля
   * @return boolean true - если модуль был успешно удален из роутера, иначе - false
   */
  public function removeController($moduleName);
}
Он очень прост в реализации, для работы он использует ini файл со следующей структурой:
GuestBook=\PPHP\model\modules\guestBook\Controller
При запросе у центрального контроллера модуля GuestBook, он передает запрос к контроллеру model\modules\guestBook\Controller
Приемник
Данный класс включает механизмы получения данных из массива $_REQUEST и имеет следующий вид:
<?php
/**
 * Класс предоставляет доступ к виду и позволяет обмениваться с ним сообщениями.
 */
class ViewProvider implements \PPHP\tools\patterns\singleton\Singleton{
use \PPHP\tools\patterns\singleton\TSingleton;

  /**
   * Метод возвращает сообщение, полученное от вида.
   * @return mixed|null Содержимое сообщения или null - если сообщение не было передано.
   */
  public function getMessage(){
    if(!isset($_REQUEST['ViewMessage'])){
      return null;
    }
    return json_decode($_REQUEST['ViewMessage']);
  }

  /**
   * Метод передает сообщение виду.
   * @param mixed $message Содержимое сообщения.
   */
  public function sendMessage($message){
    echo json_encode($message);
  }
}
Как можно заметить, это очень простой класс, инкапсулирующий механизмы получения данных от клиента.
Центральный контроллер (единая точка входа)
Данный класс вызывается всегда, при передаче команд от вида в модель. Он реализован следующим образом:
<?php
/**
 * Класс является центральным контроллером системы и отвечает за вызов и передачу модулю сообщений вида, а так же за возврат ему ответа модуля.
 */
class CentralController{
  /**
   * Данный ментод отвечает за передачу модулю сообщения и возврат ответа.
   * @static
   * @throws ModuleNotFoundException Выбрасывается в случае, если требуемый модуль не зарегистрирован в системе.
   */
  public static function main(){
    // Используем приемник для получения текущей команды
    $viewProvider = \PPHP\services\view\ViewProvider::getInstance();
    $viewMessage = $viewProvider->getMessage();

    // Используем роутер для получения ответственного контроллера
    $modulesRouter = \PPHP\services\modules\ModulesRouter::getInstance();
    if(!$modulesRouter->isModuleExists($viewMessage->module)){
      throw new ModuleNotFoundException('Требуемого модуля "' . $viewMessage->module . '" не существует.');
    }
    $controller = $modulesRouter->getController($viewMessage->module);
    $controller = $controller::getInstance();

    // Запускаем в ответственном контроллере его метод-обработчик. Ответ передаем обратно в вид.
    $send = new \stdClass();
    try{
      $send->answer = $controller->main(is_null($viewMessage->message)? null : $viewMessage->message);
    }
    catch(\Exception $e){
      $send->exception = $e;
    }
    // Возврат ответа модели в вид с помощью Приемника
    $viewProvider->sendMessage($send);
  }
}
// Любой запуск скрипта приведет к запуску центрального контроллера
CentralController::main();
Интерфейс контроллеров модуля
Как можно заметить, центральный контроллер всегда использует однин и тот же метод ответственного контроллера - main - значит нужно чтобы все контроллеры реализовали
этот метод, для этого определим интерфейс контроллеров модуля:
<?php
/**
 * Все конкретные контроллеры модулей должны реализовывать данный интерфейс.
 */
abstract class ModuleController implements \PPHP\tools\patterns\metadata\reflection\Reflect, \PPHP\tools\patterns\singleton\Singleton{
// Все контроллеры модуля являются Singletom и могут быть аннотированы
use \PPHP\tools\patterns\metadata\reflection\TReflect;
use \PPHP\tools\patterns\singleton\TSingleton;

  /**
   * Точка входа в контроллер модуля.
   * @abstract
   * @param mixed|null $message Сообщение, переданное от вида.
   * @return mixed Возвращаемые виду данные.
   */
  abstract public function main($message = null); // Данный метод отвечает за перенаправления запросов от вида в модель
}

Пример контроллера
Простейший контроллер:
<?php
namespace PPHP\model\modules\guestBook;

class Controller extends \PPHP\model\classes\ModuleController{
  public function main($message = null){
    return 0;
  }
}
Данный контроллер ничего не обрабатывает и всегда возвращает ответ 0, но он показывает как реализовать контроллер модуля. Достаточно создать класс, реализующий интерфейс ModuleController и инициализировать контроллер в роутере добавив запись в ini файл:
GuestBook=\PPHP\model\modules\guestBook\Controller
Вы наверно уже заметили, что данная запись как раз указывает на нужный контроллер: namespace PPHP\model\modules\guestBook; class Controller ... Так роутер и находит требуемые контроллеры. Все просто.
Примеры
Выкладываю мою незаконченную систему управления с использованием изложенных выше принципов. Если будут вопросы, прошу в приват, или, если администрация разрешит, можно создать отдельную тему. Надеюсь, будет актуально smile
Архив: http://upwap.ru/2607316
Пароль: 3129
Added:
MVC, CMS
Rating: +5
Views: 3178
Comments (4) »