
Chain of Responsibility を PHP で
Chain of Responsibility は、 振る舞いに関するデザインパターンの一つで、 潜在的なハンドラーの連鎖の上を、 ハンドラーのどれかが処理するまで、 リクエストを回していきます。
このパターンを利用すると、 送り手のクラスと受け手の具象クラスとを結合することなく、 複数のオブジェクトにリクエストを処理する機会を与えることができます。 連鎖は実行時に、 標準のハンドラー・インターフェースに従うハンドラーから動的に構成されます。
複雑度:
人気度:
使用例: Chain of Responsibility パターンは、 PHP の種々のフレームワークで結構よく使われます。 議論の余地はありますが、 PHP でこのパターンを使う最も有名な例としては、 PSR-15 に記述されている PHP リクエスト・ミドルウェアがあげられます。
見つけ方: 共通のインターフェースに従うオブジェクトのグループで、 実作業を行うメソッドが、 別のオブジェクトの同一メソッドを呼ぶことから、 このパターンを識別できます。
概念的な例
この例は、 Chain of Responsibility デザインパターンの構造を説明するためのものです。 以下の質問に答えることを目的としています:
- どういうクラスからできているか?
- それぞれのクラスの役割は?
- パターンの要素同士はどう関係しているのか?
ここでパターンの構造を学んだ後だと、 これに続く、 現実世界の 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 の世界で最も広く知られている Chain of Responsibility (CoR) パターンの使用は、 HTTP リクエストのミドルウェアにあります。 これらは最も人気のある PHP フレームワークによって実装されており、 PSR-15 の一部として標準化されています。
以下のように動作します。 HTTP リクエストがアプリによって処理されるためには、 ミドルウェア・オブジェクトの積み重ねを通過する必要があります。 各ミドルウェアは、 リクエストの処理を拒否するか、 次のミドルウェアに渡すことができます。 リクエストがすべてのミドルウェアを正常に通過して初めて、 アプリの主ハンドラーが最終的にそれを処理できます。
このやり方は、 パターンの元々の意図の逆のようであることにお気づきかもしれません。 その通りで、 典型的な実装では、 リクエストは現ハンドラーが取り扱えない場合にだけ次に渡されます。 それに対して、 ミドルウェアは、 アプリがリクエストを取り扱えると思われる場合に、 連鎖を通してリクエストを渡します。 にもかかわらず、 ミドルウェア・オブジェクトは連鎖をなしているため、 概念全体は CoR パターンとみなされます。
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!