Статьи

Введение в шаблон переднего контроллера, часть 2

Фронт-контроллеры действуют как централизованные агенты в приложении, основной задачей которого является отправка команд, статически или динамически, предопределенным обработчикам, таким как контроллеры страниц, ресурсы REST или почти все остальное, что приходит на ум.

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

Одна из лучших особенностей фронт-контроллеров заключается в том, что вы можете поддерживать их работу в виде узких структур, просто направляя и отправляя входящие запросы, или вы можете позволить своей дикой стороне показать и реализовать полноценный контроллер RESTful, способный анализировать HTTP-глаголы, приспосабливая предварительно / отправка перехватчиков и тому подобное, все за единым API. Но хотя этот подход привлекателен, он нарушает принцип единой ответственности (SRP) и противоречит самой природе ООП, которая активно подталкивает делегирование различных задач к нескольким детализированным объектам.

Значит ли это, что я просто еще одна грешная душа, которая осмелилась нарушить заповеди СРП? Ну, в некотором смысле, я. Поэтому я хотел бы смыть с себя все грехи, показав вам, как легко развернуть небольшую, но расширяемую среду HTTP, способную задействовать фронт-контроллер вместе с автономным маршрутизатором и диспетчером. Кроме того, весь цикл запроса / ответа будет независимо обрабатываться парой повторно используемых классов, которые, естественно, вы сможете настроить по своему усмотрению.

С таким огромным распространением доступных HTTP-фреймворков, упакованных с полнофункциональными компонентами, кажется абсурдным реализовывать с нуля фронт-контроллер, который направляет и отправляет запросы через несколько модульных классов, даже если они сохраняют сущность SRP. В скромной попытке избежать суда за изобретение колеса, некоторые куски моей пользовательской реализации будут вдохновлены изящной библиотекой EPHPMVC, написанной Ларсом Стройным.

Рассечение цикла Запрос / Маршрут / Отправка / Ответ

Первая задача, которую мы должны решить, — определить пару классов, отвечающих за моделирование данных и поведения типичного цикла HTTP-запроса / ответа. Вот первый, связанный с интерфейсом, который он реализует:

class Request {

  public function __construct($uri, $params) { 
    $this->uri = $uri;
    $this->params = $params;
  }
 
  public function getUri() {
    return $this->uri;
  }
 
  public function setParam($key, $value) {
    $this->params[$key] = $value;
    return $this;
  }
 
  public function getParam($key) {
    if (!isset($this->params[$key])) {
      throw new \InvalidArgumentException("The request parameter with key '$key' is invalid."); 
    }
    return $this->params[$key];
  }
 
  public function getParams() {
    return $this->params;
  }
}

Класс Request инкапсулирует входящий URI вместе с массивом параметров и моделирует чрезвычайно скелетный HTTP-запрос. Для краткости дополнительные элементы данных, такие как набор методов, связанных с рассматриваемым запросом, были намеренно оставлены за пределами рисунка. Если вам хочется бросить их в класс, продолжайте в том же духе.

Наличие тонкой оболочки для HTTP-запросов, которая сама живет счастливо, прекрасно, но в конечном итоге бесполезно, если не связано с аналогом, который имитирует данные и поведение типичного HTTP-ответа. Давайте исправим и создадим этот дополнительный компонент:

 class Response {
  public function __construct($version) {
    $this->version = $version;
  }
 
  public function getVersion() {
    return $this->version;
  }
 
  public function addHeader($header) {
    $this->headers[] = $header;
    return $this;
  }
 
  public function addHeaders(array $headers) {
    foreach ($headers as $header) {
      $this->addHeader($header);
    }
    return $this;
  }
 
  public function getHeaders() {
    return $this->headers;
  }
 
  public function send() {
    if (!headers_sent()) {
      foreach($this->headers as $header) {
        header("$this->version $header", true);
      }
    } 
  }
}

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

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

Вот пакет классов, которые запускают и запускают модуль маршрутизации:

 class Route {

  public function __construct($path, $controllerClass) {
    $this->path = $path;
    $this->controllerClass = $controllerClass;
  }
 
  public function match(RequestInterface $request) {
    return $this->path === $request->getUri();
  }
 
  public function createController() {
   return new $this->controllerClass;
  }
}

 
class Router {
  public function __construct($routes) {
    $this->addRoutes($routes);
  }
 
  public function addRoute(RouteInterface $route) {
    $this->routes[] = $route;
    return $this;
  }
 
  public function addRoutes(array $routes) {
    foreach ($routes as $route) {
      $this->addRoute($route);
    }
    return $this;
  }
 
  public function getRoutes() {
    return $this->routes;
  }
 
  public function route(RequestInterface $request, ResponseInterface $response) {
    foreach ($this->routes as $route) {
      if ($route->match($request)) {
        return $route;
      }
    }
    $response->addHeader("404 Page Not Found")->send();
    throw new \OutOfRangeException("No route matched the given URI.");
  }
}

Как и следовало ожидать, существует множество вариантов на выбор, когда речь идет о реализации функционального механизма маршрутизации. Вышеизложенное, по крайней мере, на мой взгляд, раскрывает как прагматичное, так и прямолинейное решение. Он определяет независимый класс Route, который связывает путь с данным контроллером действий, и простой маршрутизатор, ответственность которого ограничивается проверкой соответствия хранимого маршрута URI, связанному с конкретным объектом запроса.

Чтобы все окончательно уладить, нам нужно настроить быстрый диспетчер, который можно будет запускать параллельно с предыдущими классами. Класс ниже делает именно это:

 class Dispatcher {

  public function dispatch($route, $request, $response)
    $controller = $route->createController();
    $controller->execute($request, $response);
  }
}

Сканируя Диспетчер, вы заметите две вещи. Во-первых, это не несет никакого государства. Во-вторых, он неявно предполагает, что каждый контроллер действий будет выполняться под поверхностью метода execute ().

Если хотите, это может быть изменено в пользу более гибкой схемы (первое, что приходит мне в голову, это настройка реализации класса Route), но ради простоты я оставлю диспетчер без изменений.

К настоящему времени вы, вероятно, задаетесь вопросом, как и куда поместить фронтальный контроллер, способный объединить все предыдущие классы. Не беспокойся, так как это дальше!

Реализация настраиваемого фронт-контроллера

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

 class FrontController {

  public function __construct($router, $dispatcher) {
    $this->router = $router;
    $this->dispatcher = $dispatcher;
  }
 
  public function run(RequestInterface $request, ResponseInterface $response) {
    $route = $this->router->route($request, $response);
    $this->dispatcher->dispatch($route, $request, $response);
  }
}

Все, что делает класс FrontController, — это использует его метод run () для маршрутизации и отправки данного запроса соответствующему контроллеру действий, используя закулисную функциональность его соавторов. Если вы хотите, метод может быть намного толще и инкапсулировать кучу дополнительных реализаций, таких как хуки до / после отправки и так далее. Я оставлю это как домашнее задание для вас на случай, если вы захотите добавить новую метку в пояс разработчика.

Чтобы увидеть, действительно ли настроенный фронт-контроллер функционирует так, как кажется, давайте создадим пару контроллеров банальных действий, которые реализуют метод execute ():

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

Следующий фрагмент показывает, как это сделать в двух словах:

 $request = new Request("http://example.com/test/");
 
$response = new Response;
 
$route1 = new Route("http://example.com/test/", "Acme\\Library\\Controller\\TestController");
 
$route2 = new Route("http://example.com/error/", "Acme\\Library\\Controller\\ErrorController");
 
$router = new Router(array($route1, $route2));
 
$dispatcher = new Dispatcher;
 
$frontController = new FrontController($router, $dispatcher);
 
$frontController->run($request, $response);

Несмотря на то, что сценарий выглядит несколько переполненным, потому что он сначала проходит через несколько маршрутов, которые передаются во внутреннюю часть фронт-контроллера, он демонстрирует, как заставить вещи работать и вызывать контроллеры действий довольно простым способом. Кроме того, в примере экземпляр TestController будет вызываться во время выполнения, поскольку он эффективно соответствует первому маршруту. Само собой разумеется, что маршрутизацию можно настраивать сверху вниз, так как вызов другого контроллера действий — это просто вопрос передачи другого URI объекту Request и, конечно же, соответствующего маршрута.

Несмотря на все предварительные настройки, требуемые в процессе реализации, фактическая красота этого подхода основывается на модульности, предоставляемой каждым классом, участвующим в цикле запроса / маршрута / отправки / ответа. Больше нет необходимости разбираться со странностями монолитного фронт-контроллера, не говоря уже о том, что большинство объектов, включенных во всю транзакцию, таких как дуэт «запрос / ответ», могут быть легко использованы повторно в различных контекстах или заменены более надежные реализации.

Заключительные мысли

Хотя академическое определение фронт-контроллера кажется довольно жестким, поскольку оно описывает шаблон как централизованный, основанный на командах механизм, суженный только для диспетчеризации запросов, правда в том, что в реальном мире число подходов, которые можно использовать для получения функциональная реализация совсем не скудна.

В этом кратком обзоре моя цель состояла в том, чтобы просто продемонстрировать, как создать, по крайней мере, пару настраиваемых фронт-контроллеров с прагматической точки зрения, обратившись сначала к компактной, узкой реализации, где процессы маршрутизации и диспетчеризации были упакованы в рамках одного единственного класс catch-all и, во-вторых, более детальное решение, в котором выполнение рассматриваемых процессов было разбито и делегировано нескольким мелкозернистым классам.

Есть еще много дополнительных опций, которые стоит посмотреть, кроме представленных здесь, каждый из которых имеет свои преимущества и недостатки. Как обычно, то, что вы должны подобрать, зависит исключительно от вкуса вашего привередливого вкуса.