Core points
The front-end controller is like a centralized proxy in an application, and its main area of focus is to statically or dynamically assign commands to predefined handlers such as page controllers, REST resources, or anything else that comes to mind. Building at least one simple front-end controller is a very useful experience to understand its details, and to promote this idea from a pragmatic point of view, I introduced in an introductory post the implementation of a artificial front-end controller that will route and all the logic required to dispatch requests is packaged within the boundaries of a class. One of the best aspects of front-end controllers is that you can keep them as compact structures, route and dispatch incoming requests only, or you can show your creativity and implement a fully functional RESTful controller capable of parsing HTTP Verbs, adaptation pre/post dispatch hooks, etc., all behind a unified API. However, while this approach is attractive, it violates the single responsibility principle (SRP) and the nature of OOP itself proactively delegating different tasks to multiple fine-grained objects. So, does this mean I am just another sinful soul who dares to break the SRP precepts? In a sense, yes. So I want to eliminate my sin by showing you how to easily deploy a small but scalable HTTP framework that is able to work with front-end controllers with standalone routers and schedulers. In addition, the entire request/response cycle will be handled independently by several reusable classes that can naturally be adjusted at will. Given that there are a large number of fully-featured components-packaged HTTP frameworks available, it seems ridiculous to implement a front-end controller that routes and dispatches requests from scratch even though these classes retain the nature of SRP. To avoid being judged for reinventing the wheel, some parts of my custom implementation will be inspired by the clever EPHPMVC library written by Lars Strojny.
Analyze request/routing/scheduling/response cycle
The task we should solve first is to define several classes responsible for simulating data and behavior of typical HTTP request/response cycles. This is the first class, and the interface it implements:
<code>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; } }</code>
The Request class encapsulates the incoming URI and parameter array and simulates an extremely simple HTTP request. For brevity, additional data members such as the method set associated with the relevant request have been intentionally excluded. If you want to add them to the class, continue. Having a thin HTTP request wrapper that exists independently is good, but it will end up being useless without coupling with the corresponding parts of the data and behavior that simulates a typical HTTP response. Let's fix and build this supplemental component:
<code>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); } } } }</code>
The Response class is undoubtedly more active than its partner Request. It acts as a base container that allows you to stack HTTP headers at will and is able to send them to the client. Since these classes perform their operations independently, it's time to start building the next part of the front-end controller. In a typical implementation, the routing/dispatch process is mostly encapsulated in the same approach, which is frankly not too bad at all. However, in this case, it is better to break down these processes and delegate them to different classes. In this way, things will be more balanced in terms of equal responsibility. Here are the batches of classes that make the routing module run:
<code>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."); } }</code>
As one might expect, there are many options when implementing functional routing mechanisms. At least in my opinion, the above method is both practical and direct. It defines a separate Route class that binds the path to a given operation controller, and a simple router whose responsibilities are limited to checking whether the stored route matches the URI associated with a specific request object. To finally solve the problem, we need to set up a quick scheduler that can be used side by side with the previous class. This is how the following class does:
<code>class Dispatcher { public function dispatch($route, $request, $response) $controller = $route->createController(); $controller->execute($request, $response); } }</code>
Scan the Dispatcher and you will notice two things. First, it does not carry any state. Second, it implicitly assumes that each operation controller will run under the surface of the execute() method. This can be refactored to a slightly more flexible pattern if you want (the first thing that comes to mind is to tweak the implementation of the Route class), but for simplicity, I'll keep the scheduler unchanged. So far, you might be wondering how to place a front-end controller that is able to combine all previous classes together. Don't worry, next is!
(Due to space limitations, subsequent content will be truncated. Please provide the rest of the content and I will continue to complete the pseudo-original.)
The above is the detailed content of An Introduction to the Front Controller Pattern, Part 2. For more information, please follow other related articles on the PHP Chinese website!