控制器模式
Front Controllers act like centralized agents in an application whose primary area of concern is to dispatch commands, either statically or dynamically, to predefined handlers such as page controllers, REST resources, or pretty much anything else that comes to mind.
在应用程序中,前端控制器的作用就像集中式代理,主要关注的领域是静态地或动态地向预定义的处理程序(例如页面控制器,REST资源或几乎所有其他想想的事情)调度命令。
Building at least a naive front controller is a pretty instructional experience in understanding the nitty-gritty of them, and to promote this idea from a pragmatic standpoint, I went through the implementation of a contrived front controller in the introductory article which packaged all the logic required for routing and dispatching requests inside the boundaries of a single class.
至少构建一个幼稚的前端控制器对于理解它们的实质是非常有益的指导经验,并且为了从务实的角度推广这种想法,我在介绍性文章中介绍了人为设计的前端控制器的实现,该封装了所有逻辑在单个类的边界内路由和调度请求所必需的。
One of the best things about front controllers is that you can keep them running as tight structures, just routing and dispatching incoming requests, or you can let your wild side show and implement a full-fledged RESTful controller capable of parsing HTTP verbs, accommodating pre/post dispatch hooks, and the like, all behind a unified API. But while this approach is appealing, it breaks the Single Responsibility Principle (SRP) and goes against the nature of OOP itself which actively pushes the delegation of different tasks to several fine-grained objects.
关于前端控制器的最好的事情之一是,您可以让它们作为紧密的结构运行,仅路由和调度传入的请求,或者可以让您的野蛮人显示并实现一个成熟的RESTful控制器,该控制器能够解析HTTP动词,并容纳/ post派发钩等,它们都位于统一的API之后。 但是,尽管这种方法很吸引人,但它打破了单一职责原则(SRP),并违背了OOP本身的本质,OOP本身将不同任务的委派推向多个细粒度的对象。
So does this mean I’m just another sinful soul who dared to break from the commandments of the SRP? Well, in a sense I am. So I’d like to wash away my sins by showing you how easy is to deploy a small, yet extensible, HTTP framework capable of putting to work a front controller along with the ones of a standalone router and a dispatcher. Plus, the whole request/response cycle will be independently handled by a couple of reusable classes, which naturally you’ll be able to tweak at will.
那么这是否意味着我只是另一个敢于违反SRP诫命的罪人? 好吧,从某种意义上说我是。 因此,我想通过向您展示部署一个小型但可扩展的HTTP框架(能够使前端控制器以及独立路由器和调度程序一起使用)的容易程度来消除我的过失。 另外,整个请求/响应周期将由几个可重用的类独立处理,您自然可以随意进行调整。
With such a huge proliferation of HTTP frameworks available packaged with full-featured components, it seems absurd to implement from scratch a front controller that routes and dispatches requests through a few modular classes, even if these ones retain the essence of the SRP. In a humble attempt to avoid being judged for reinventing the wheel, some chunks of my custom implementation will be inspired by the nifty EPHPMVC library written by Lars Strojny.
随着功能强大的组件打包提供的HTTP框架的大量增加,从头开始实现通过几个模块化类路由和调度请求的前端控制器似乎是荒谬的,即使这些保留了SRP的本质。 为了避免因重新发明轮子而被判断为谦虚,我的自定义实现的某些部分将受到Lars Strojny编写的漂亮EPHPMVC库的启发。
剖析请求/路由/调度/响应周期 (Dissecting the Request/Route/Dispatch/Response Cycle)
The first task we should tackle is defining a couple of classes charged with modeling the data and behavior of a typical HTTP request/response cycle. Here’s the first one, coupled to the interface that it implements:
我们应该解决的第一个任务是定义几个类,这些类负责对典型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;
}
}
The Request class encapsulates an incoming URI along with an array of parameters and models an extremely skeletal HTTP request. For the sake of brevity, additional data members such as the set of methods associated to the request in question have been deliberately left outside of the picture. If you feel in the mood to drop them into the class, go ahead and do so.
Request类封装了传入的URI和一系列参数,并模拟了一个极其骨架的HTTP请求。 为了简洁起见,故意将其他数据成员(例如与所讨论的请求关联的方法集)留在图片之外。 如果您有意将其加入课堂,请继续这样做。
Having a slim HTTP request wrapper living happily on its own is all well and fine sure, but ultimately useless if not coupled to the counterpart that mimics the data and behavior of a typical HTTP response. Let’s fix and build up this complementary component:
毫无疑问,让苗条的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);
}
}
}
}
The Response class is unquestionably a more active creature than its partner Request. It acts like a basic container which allows you to stack up HTTP headers at will and is capable of sending them out to the client too.
毫无疑问,Response类比其伙伴Request更为活跃。 它的作用就像一个基本的容器,它使您可以随意堆积HTTP标头,也可以将它们发送到客户端。
With these classes doing their thing in isolation, it’s time to tackle the next part in the construction of a front controller. In a typical implementation, the routing/dispatching processes are most of the time encapsulated inside the same method, which frankly speaking isn’t that bad at all. In this case, however, it’d be nice to break down the processes in question and delegate them to different classes. This way, things are balanced a little more in the equally of their responsibilities.
这些类隔离地完成工作,是时候解决前端控制器构造的下一部分了。 在典型的实现中,路由/调度过程大部分时间都封装在同一方法中,坦白地说,这一点也不差。 但是,在这种情况下,最好将有问题的进程分解并将它们委托给不同的类。 这样,事物在职责上的平衡就得到了更多的平衡。
Here’s the batch of classes that get the routing module up and running:
这是使路由模块启动并运行的一批类:
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.");
}
}
As one might expect, there’s a plethora of options to choose from when it comes to implementing a functional routing mechanism. The above, at least in my view, exposes both a pragmatic and straightforward solution. It defines an independent Route class that ties a path to a given action controller, and a simple router whose responsibility is limited to checking if a stored route matches the URI associated to a specific request object.
正如人们可能期望的那样,在实现功能路由机制时,有很多选择。 至少在我看来,以上内容揭示了务实而直接的解决方案。 它定义了一个独立的Route类,该类将路径绑定到给定的动作控制器,还定义了一个简单的路由器,其职责仅限于检查存储的路由是否匹配与特定请求对象关联的URI。
To get things finally sorted out, we would need to set up a swift dispatcher that can be put to work side by side with the previous classes. The below class does exactly that:
为了使问题最终得到解决,我们需要建立一个快速的调度程序,该调度程序可以与以前的类一起使用。 下面的类正是这样做的:
class Dispatcher {
public function dispatch($route, $request, $response)
$controller = $route->createController();
$controller->execute($request, $response);
}
}
Scanning the Dispatcher, you’ll notice two things about. First, it doesn’t carry any state. Second, it implicitly assumes that each action controller will run under the surface of an execute() method.
扫描分派器,您会注意到两件事。 首先,它没有任何状态。 其次,它隐式地假设每个动作控制器将在execute()方法的表面下运行。
This can be refactored in favor of a slightly more flexible schema if you wish (the first thing that comes to my mind is tweaking the implementation of the Route class), but for the sake of simplicity I’ll keep the dispatcher untouched.
如果愿意的话,可以将其重构为稍微灵活的模式(我想到的第一件事是调整Route类的实现),但是为了简单起见,我将保持调度程序不变。
By now you’re probably wondering how and where to drop a front controller capable of bring all of the previous classes together. Don’t be anxious, as that’s next!
现在,您可能想知道如何以及在哪里放置能够将所有先前的类组合在一起的前端控制器。 不要着急,因为那是下一个!
实施可定制的前端控制器 (Implementing a Customizable Front Controller)
We’ve reached the moment we’ve all been waiting for since the very beginning, implementing the long awaited front controller. But if you were expecting the implementation to be pretty much some kind of epic quest, given the number of classes that we dropped up front, I’m afraid you’ll be disappointed. In fact, creating the controller boils down to just defining a class that shields the functionality of the router and the dispatcher behind a ridiculously simple API:
从一开始,我们就已经等待了所有的时间,实现了期待已久的前端控制器。 但是,如果您期望实现几乎是某种史诗般的追求,考虑到我们预先放弃的类的数量,恐怕您会失望的。 实际上,创建控制器可以归结为仅定义一个类,该类在一个非常简单的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);
}
}
All that the FrontController class does is employ its run() method for routing and dispatching a given request to the corresponding action controller by using the behind-the-scenes functionality of its collaborators. If you’d like, the method could be a lot fatter and encapsulate a bunch of additional implementation, such as pre/post dispatch hooks and so forth. I’ll leave this as homework for you in case you want to add a new notch to your developer belt.
FrontController类所做的全部工作就是利用其run()方法,通过使用其协作者的幕后功能来路由给定的请求并将其分配给相应的动作控制器。 如果您愿意,该方法可能会胖很多,并封装了许多其他实现,例如pre / post派发钩子等。 如果您想为开发人员带一个新的标记,我将把它留给您作为家庭作业。
To see if the front controller set up is actually as functional as it seems, let’s create a couple of banal action controllers which implement an execute() method:
要查看前端控制器的设置是否真的像看起来那样功能正常,让我们创建几个平常的行为控制器,这些控制器实现execute()方法:
In this case, the sample action controllers are pretty simple creatures that don’t do anything particularly useful other than outputting a couple of trivial messages to screen. The point here is to demonstrate how to call them via the earlier front controller, and pass along the request and response objects for some eventual further processing.
在这种情况下,示例动作控制器是非常简单的生物,除了将一些琐碎的消息输出到屏幕上之外,它没有做任何特别有用的事情。 这里的重点是演示如何通过较早的前端控制器调用它们,并传递请求和响应对象以进行某些最终的进一步处理。
The following snippet shows how to accomplish this in a nutshell:
以下代码段简要说明了如何完成此操作:
$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);
Even though the script looks somewhat crowded because it first goes through factoring a couple of routes which are passed into the front controller’s internals, it demonstrates how to get things rolling and call action controllers in a pretty straightforward fashion. Moreover, in the example an instance of TestController will be invoked at runtime, as it effectively matches the first route. Needless to say that the routing can be customized from top to bottom, as calling other action controller is just a matter of passing around a different URI to the Request object, and of course a matched route.
尽管该脚本看起来有些拥挤,因为它首先经历了传递到前端控制器内部的几条路由,但是它演示了如何使事情滚动并以非常简单的方式调用动作控制器。 此外,在该示例中,TestController的实例将在运行时被调用,因为它有效地匹配了第一条路线。 不用说,可以自上而下地定制路由,因为调用其他动作控制器只是将不同的URI传递给Request对象,当然还有匹配的路由。
Despite of all the up front setup required through the implementation process, the actual beauty of this approach rests on the modularity exposed by each class involved in the request/route/dispatch/response cycle. There’s no need to deal with the oddities of a monolithic front controller anymore, not to mention the fact that most of the objects included in the whole transaction, such as the Request/Response duet, can be easily reused in different contexts or swapped out by more robust implementations.
尽管在实现过程中需要进行所有的前期设置,但此方法的实际优点还是取决于请求/路由/调度/响应周期中涉及的每个类公开的模块化。 不再需要处理整体式前端控制器的不足之处,更不用说整个事务中包含的大多数对象(例如请求/响应二重奏)都可以轻松地在不同的上下文中重用或替换为更强大的实现。
总结思想 (Closing Thoughts)
Though the academic definition of a front controller seems to be pretty rigid as it describes the pattern as a centralized, command-based mechanism narrowed to just dispatching requests, the truth is that in the real world the number of approaches that can be used for getting a functional implementation is anything but scarce.
尽管前端控制器的学术定义似乎相当僵化,因为它将模式描述为一种集中的,基于命令的机制,范围仅限于调度请求,但事实是,在现实世界中,可用于获取信息的方法数量众多。功能实现几乎是稀缺的。
In this quick roundup, my goal was just to demonstrate how to create at least a couple of customizable front controllers from a pragmatic standpoint by appealing first to a compact, tight implementation, where the routing and dispatching processes were packaged inside the limits of one single, catch-all class, and second, through a more granular solution, where the execution of the processes in question were dissected and delegated to a few fine-grained classes.
在此快速综述中,我的目的只是演示如何从务实的角度创建至少两个可定制的前端控制器,方法是首先呼吁紧凑,紧凑的实现,其中路由和调度过程打包在一个单一的限制内,通俗易懂的类,其次,通过更细化的解决方案,将有问题的流程的执行过程分解为多个细粒度的类。
There’s still a lot of additional options out there worth looking, other than the ones showcased here, with each one having their own benefits and drawbacks. As usual, which one you should to pick up depends exclusively on the taste of your picky palate.
除了这里展示的那些之外,还有许多其他值得一看的选项,每个选项都有其自身的优点和缺点。 像往常一样,您应该选择哪一种完全取决于您的挑剔口味。
控制器模式
本文深入探讨了前端控制器模式在应用程序中的角色,解释了其如何作为集中式代理调度请求到预定义的处理程序,如页面控制器和REST资源。通过构建一个简单的前端控制器,文章展示了如何封装路由和调度逻辑,同时讨论了与单一职责原则的关系。
375

被折叠的 条评论
为什么被折叠?



