
PHP 责任链模式讲解和代码示例
责任链是一种行为设计模式, 允许你将请求沿着处理者链进行发送, 直至其中一个处理者对其进行处理。
该模式允许多个对象来对请求进行处理, 而无需让发送者类与具体接收者类相耦合。 链可在运行时由遵循标准处理者接口的任意处理者动态生成。
复杂度:
流行度:
使用示例: 责任链模式在 PHP 程序中并不常见, 因为它仅在代码与对象链打交道时才能发挥作用。 可以说在 PHP 中, 该模式最著名的例子之一是 PSR-15 中所描述的 HTTP 请求中间件
识别方法: 该模式可通过一组对象的行为方法间接调用其他对象的相同方法来识别, 而且所有对象都会遵循相同的接口。
概念示例
本例说明了责任链设计模式的结构并重点回答了下面的问题:
- 它由哪些类组成?
- 这些类扮演了哪些角色?
- 模式中的各个元素会以何种方式相互关联?
了解该模式的结构后, 你可以更容易地理解下面基于真实世界的 PHP 应用案例。
index.php: 概念示例
<?php
namespace RefactoringGuru\ChainOfResponsibility\Conceptual;
/**
* The Handler interface declares a method for building the chain of handlers.
* It also declares a method for executing a request.
*/
interface Handler
{
public function setNext(Handler $handler): Handler;
public function handle(string $request): ?string;
}
/**
* The default chaining behavior can be implemented inside a base handler class.
*/
abstract class AbstractHandler implements Handler
{
/**
* @var Handler
*/
private $nextHandler;
public function setNext(Handler $handler): Handler
{
$this->nextHandler = $handler;
// Returning a handler from here will let us link handlers in a
// convenient way like this:
// $monkey->setNext($squirrel)->setNext($dog);
return $handler;
}
public function handle(string $request): ?string
{
if ($this->nextHandler) {
return $this->nextHandler->handle($request);
}
return null;
}
}
/**
* All Concrete Handlers either handle a request or pass it to the next handler
* in the chain.
*/
class MonkeyHandler extends AbstractHandler
{
public function handle(string $request): ?string
{
if ($request === "Banana") {
return "Monkey: I'll eat the " . $request . ".\n";
} else {
return parent::handle($request);
}
}
}
class SquirrelHandler extends AbstractHandler
{
public function handle(string $request): ?string
{
if ($request === "Nut") {
return "Squirrel: I'll eat the " . $request . ".\n";
} else {
return parent::handle($request);
}
}
}
class DogHandler extends AbstractHandler
{
public function handle(string $request): ?string
{
if ($request === "MeatBall") {
return "Dog: I'll eat the " . $request . ".\n";
} else {
return parent::handle($request);
}
}
}
/**
* The client code is usually suited to work with a single handler. In most
* cases, it is not even aware that the handler is part of a chain.
*/
function clientCode(Handler $handler)
{
foreach (["Nut", "Banana", "Cup of coffee"] as $food) {
echo "Client: Who wants a " . $food . "?\n";
$result = $handler->handle($food);
if ($result) {
echo " " . $result;
} else {
echo " " . $food . " was left untouched.\n";
}
}
}
/**
* The other part of the client code constructs the actual chain.
*/
$monkey = new MonkeyHandler();
$squirrel = new SquirrelHandler();
$dog = new DogHandler();
$monkey->setNext($squirrel)->setNext($dog);
/**
* The client should be able to send a request to any handler, not just the
* first one in the chain.
*/
echo "Chain: Monkey > Squirrel > Dog\n\n";
clientCode($monkey);
echo "\n";
echo "Subchain: Squirrel > Dog\n\n";
clientCode($squirrel);
Output.txt: 执行结果
Chain: Monkey > Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
Monkey: I'll eat the Banana.
Client: Who wants a Cup of coffee?
Cup of coffee was left untouched.
Subchain: Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
Banana was left untouched.
Client: Who wants a Cup of coffee?
Cup of coffee was left untouched.
真实世界示例
在 PHP 世界中, 责任链 (CoR) 模式在 HTTP 请求中间件中的使用是最广为人知的。 绝大多数流行的 PHP 框架都会使用该模式来实现 HTTP 请求中间件, 它甚至被标准化而成为了 PSR-15 的一个组成部分。
它的运作方式是这样的: HTTP 请求必须通过一个中间件对象堆栈才能被程序处理。 每个中间件可以拒绝进一步处理该请求, 也可以将其传递给下一个中间件。 当请求成功通过所有中间件后, 程序的主处理者才能最终对其进行处理。
你可能已经注意到这种方法在某种程度上来说颠倒了该模式的原始意图。 的确, 在通常的实现中, 只有在当前处理者无法对请求进行处理时, 请求才能沿着链进行传递; 而中间件认为程序可以处理该请求时, 才会沿着链将其传递下去。 尽管如此, 由于中间件是相互连接的, 所以整个概念仍被认为是责任链模式的示例。
index.php: 真实世界示例
<?php
namespace RefactoringGuru\ChainOfResponsibility\RealWorld;
/**
* The classic CoR pattern declares a single role for objects that make up a
* chain, which is a Handler. In our example, let's differentiate between
* middleware and a final application's handler, which is executed when a
* request gets through all the middleware objects.
*
* The base Middleware class declares an interface for linking middleware
* objects into a chain.
*/
abstract class Middleware
{
/**
* @var Middleware
*/
private $next;
/**
* This method can be used to build a chain of middleware objects.
*/
public function linkWith(Middleware $next): Middleware
{
$this->next = $next;
return $next;
}
/**
* Subclasses must override this method to provide their own checks. A
* subclass can fall back to the parent implementation if it can't process a
* request.
*/
public function check(string $email, string $password): bool
{
if (!$this->next) {
return true;
}
return $this->next->check($email, $password);
}
}
/**
* This Concrete Middleware checks whether a user with given credentials exists.
*/
class UserExistsMiddleware extends Middleware
{
private $server;
public function __construct(Server $server)
{
$this->server = $server;
}
public function check(string $email, string $password): bool
{
if (!$this->server->hasEmail($email)) {
echo "UserExistsMiddleware: This email is not registered!\n";
return false;
}
if (!$this->server->isValidPassword($email, $password)) {
echo "UserExistsMiddleware: Wrong password!\n";
return false;
}
return parent::check($email, $password);
}
}
/**
* This Concrete Middleware checks whether a user associated with the request
* has sufficient permissions.
*/
class RoleCheckMiddleware extends Middleware
{
public function check(string $email, string $password): bool
{
if ($email === "admin@example.com") {
echo "RoleCheckMiddleware: Hello, admin!\n";
return true;
}
echo "RoleCheckMiddleware: Hello, user!\n";
return parent::check($email, $password);
}
}
/**
* This Concrete Middleware checks whether there are too many failed login
* requests.
*/
class ThrottlingMiddleware extends Middleware
{
private $requestPerMinute;
private $request;
private $currentTime;
public function __construct(int $requestPerMinute)
{
$this->requestPerMinute = $requestPerMinute;
$this->currentTime = time();
}
/**
* Please, note that the parent::check call can be inserted both at the
* beginning of this method and at the end.
*
* This gives much more flexibility than a simple loop over all middleware
* objects. For instance, a middleware can change the order of checks by
* running its check after all the others.
*/
public function check(string $email, string $password): bool
{
if (time() > $this->currentTime + 60) {
$this->request = 0;
$this->currentTime = time();
}
$this->request++;
if ($this->request > $this->requestPerMinute) {
echo "ThrottlingMiddleware: Request limit exceeded!\n";
die();
}
return parent::check($email, $password);
}
}
/**
* This is an application's class that acts as a real handler. The Server class
* uses the CoR pattern to execute a set of various authentication middleware
* before launching some business logic associated with a request.
*/
class Server
{
private $users = [];
/**
* @var Middleware
*/
private $middleware;
/**
* The client can configure the server with a chain of middleware objects.
*/
public function setMiddleware(Middleware $middleware): void
{
$this->middleware = $middleware;
}
/**
* The server gets the email and password from the client and sends the
* authorization request to the middleware.
*/
public function logIn(string $email, string $password): bool
{
if ($this->middleware->check($email, $password)) {
echo "Server: Authorization has been successful!\n";
// Do something useful for authorized users.
return true;
}
return false;
}
public function register(string $email, string $password): void
{
$this->users[$email] = $password;
}
public function hasEmail(string $email): bool
{
return isset($this->users[$email]);
}
public function isValidPassword(string $email, string $password): bool
{
return $this->users[$email] === $password;
}
}
/**
* The client code.
*/
$server = new Server();
$server->register("admin@example.com", "admin_pass");
$server->register("user@example.com", "user_pass");
// All middleware are chained. The client can build various configurations of
// chains depending on its needs.
$middleware = new ThrottlingMiddleware(2);
$middleware
->linkWith(new UserExistsMiddleware($server))
->linkWith(new RoleCheckMiddleware());
// The server gets a chain from the client code.
$server->setMiddleware($middleware);
// ...
do {
echo "\nEnter your email:\n";
$email = readline();
echo "Enter your password:\n";
$password = readline();
$success = $server->logIn($email, $password);
} while (!$success);
Output.txt: 执行结果
Enter your email:
asd
Enter your password:
123
UserExistsMiddleware: This email is not registered!
Enter your email:
admin@example.com
Enter your password:
wrong
UserExistsMiddleware: Wrong password!
Enter your email:
admin@example.com
Enter your password:
letmein
ThrottlingMiddleware: Request limit exceeded!
Enter your email:
admin@example.com
Enter your password:
admin_pass
RoleCheckMiddleware: Hello, admin!
Server: Authorization has been successful!