核心要点
前端控制器就像应用程序中的集中式代理,其主要关注领域是静态或动态地将命令分派给预定义的处理程序,例如页面控制器、REST 资源或任何其他想到的东西。构建至少一个简单的前端控制器是理解其细节的非常有益的经验,为了从务实的角度推广这一想法,我在介绍性文章中介绍了一个人为的前端控制器的实现,该控制器将路由和分派请求所需的所有逻辑打包在一个类的边界内。前端控制器最好的方面之一是您可以将它们保持为紧凑的结构,只路由和分派传入请求,或者您可以展现您的创造力,并实现一个功能齐全的 RESTful 控制器,能够解析 HTTP 动词,适应预/后分派钩子等等,所有这些都在统一的 API 后面。但是,虽然这种方法很有吸引力,但它违反了单一职责原则 (SRP) 并违背了 OOP 本身主动将不同任务委托给多个细粒度对象的本质。那么,这是否意味着我只是另一个胆敢违反 SRP 戒律的有罪灵魂呢?从某种意义上说,是的。因此,我想通过向您展示如何轻松部署一个小型但可扩展的 HTTP 框架来消除我的罪恶,该框架能够与独立路由器和调度程序一起使用前端控制器。此外,整个请求/响应周期将由几个可重用的类独立处理,这些类自然可以随意调整。鉴于有大量功能齐全的组件打包的 HTTP 框架可用,从头开始实现一个通过几个模块化类路由和分派请求的前端控制器似乎很荒谬,即使这些类保留了 SRP 的本质。为了避免因重新发明轮子而受到评判,我的自定义实现的一些部分将受到 Lars Strojny 编写的巧妙的 EPHPMVC 库的启发。
剖析请求/路由/分派/响应周期
我们应该首先解决的任务是定义几个负责模拟典型 HTTP 请求/响应周期的数据和行为的类。这是第一个类,以及它实现的接口:
<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>
Request 类封装了传入的 URI 和参数数组,并模拟了一个极其简单的 HTTP 请求。为简洁起见,诸如与相关请求关联的方法集之类的附加数据成员已故意排除在外。如果您想将它们添加到类中,请继续操作。拥有一个独立存在的精简 HTTP 请求包装器很好,但如果没有与模拟典型 HTTP 响应的数据和行为的对应部分耦合,最终将毫无用处。让我们修复并构建这个补充组件:
<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>
Response 类无疑比其伙伴 Request 更活跃。它充当一个基本容器,允许您随意堆叠 HTTP 标头,并且能够将它们发送到客户端。由于这些类独立地执行其操作,因此是时候着手构建前端控制器的下一部分了。在典型的实现中,路由/分派过程大多数情况下封装在相同的方法中,坦白地说,这根本不算太坏。但是,在这种情况下,最好将这些过程分解并将其委托给不同的类。这样,事情在责任平等方面会更加平衡。以下是使路由模块运行的类批次:
<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>
正如人们可能预期的那样,在实现功能性路由机制时,有很多选择。至少在我看来,上述方法既实用又直接。它定义了一个独立的 Route 类,该类将路径绑定到给定的操作控制器,以及一个简单的路由器,其职责仅限于检查存储的路由是否与特定请求对象关联的 URI 匹配。为了最终解决问题,我们需要设置一个快速调度程序,可以与之前的类并排使用。下面的类就是这样做的:
<code>class Dispatcher { public function dispatch($route, $request, $response) $controller = $route->createController(); $controller->execute($request, $response); } }</code>
扫描 Dispatcher,您会注意到两件事。首先,它不携带任何状态。其次,它隐式地假设每个操作控制器都将在 execute() 方法的表面下运行。如果您愿意,这可以被重构为稍微更灵活的模式(我首先想到的是调整 Route 类的实现),但为了简单起见,我将保持调度程序不变。到目前为止,您可能想知道如何在何处放置能够将所有先前类组合在一起的前端控制器。别着急,接下来就是!
(由于篇幅限制,后续内容将被截断。请提供剩余部分,我将继续完成伪原创。)
以上是前控制器图案的简介,第2部分的详细内容。更多信息请关注PHP中文网其他相关文章!