Фронт-контроллеры действуют как централизованные агенты в приложении, основной задачей которого является отправка команд, статически или динамически, предопределенным обработчикам, таким как контроллеры страниц, ресурсы 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 и, во-вторых, более детальное решение, в котором выполнение рассматриваемых процессов было разбито и делегировано нескольким мелкозернистым классам.
Есть еще много дополнительных опций, которые стоит посмотреть, кроме представленных здесь, каждый из которых имеет свои преимущества и недостатки. Как обычно, то, что вы должны подобрать, зависит исключительно от вкуса вашего привередливого вкуса.