
PHP로 작성된 커맨드
커맨드는 요청 또는 간단한 작업을 객체로 변환하는 행동 디자인 패턴입니다.
이러한 변환은 명령의 지연 또는 원격 실행, 명령 기록 저장 등을 허용합니다.
복잡도:
인기도:
사용 예시들: 커맨드 패턴은 PHP 코드에서 매우 일반적이며 작업 대기, 실행된 작업 기록 추적 및 '실행 취소' 수행에 사용됩니다.
식별: 커맨드 패턴은 다음과 같은 특징이 있습니다. 추상/인터페이스 유형(발신자)의 행동 메서드들이 있으며 이러한 메서드들은 다른 추상/인터페이스 유형(수신자)의 구현에서 메서드를 호출하며 이 메서드는 생성되는 동안 커맨드 구현으로 캡슐화되었습니다. 또 커맨드 클래스는 일반적으로 특정 작업만 수행할 수 있습니다.
개념적인 예시
이 예시는 커맨드 디자인 패턴의 구조를 보여주고 다음 질문에 중점을 둡니다:
- 패턴은 어떤 클래스들로 구성되어 있나요?
- 이 클래스들은 어떤 역할을 하나요?
- 패턴의 요소들은 어떻게 서로 연관되어 있나요?
이 패턴의 구조를 배우면 실제 PHP 사용 사례를 기반으로 하는 다음 예시를 더욱 쉽게 이해할 수 있을 것입니다.
index.php: 개념적인 예시
<?php namespace RefactoringGuru\Command\Conceptual; /** * The Command interface declares a method for executing a command. */ interface Command { public function execute(): void; } /** * Some commands can implement simple operations on their own. */ class SimpleCommand implements Command { private $payload; public function __construct(string $payload) { $this->payload = $payload; } public function execute(): void { echo "SimpleCommand: See, I can do simple things like printing (" . $this->payload . ")\n"; } } /** * However, some commands can delegate more complex operations to other objects, * called "receivers." */ class ComplexCommand implements Command { /** * @var Receiver */ private $receiver; /** * Context data, required for launching the receiver's methods. */ private $a; private $b; /** * Complex commands can accept one or several receiver objects along with * any context data via the constructor. */ public function __construct(Receiver $receiver, string $a, string $b) { $this->receiver = $receiver; $this->a = $a; $this->b = $b; } /** * Commands can delegate to any methods of a receiver. */ public function execute(): void { echo "ComplexCommand: Complex stuff should be done by a receiver object.\n"; $this->receiver->doSomething($this->a); $this->receiver->doSomethingElse($this->b); } } /** * The Receiver classes contain some important business logic. They know how to * perform all kinds of operations, associated with carrying out a request. In * fact, any class may serve as a Receiver. */ class Receiver { public function doSomething(string $a): void { echo "Receiver: Working on (" . $a . ".)\n"; } public function doSomethingElse(string $b): void { echo "Receiver: Also working on (" . $b . ".)\n"; } } /** * The Invoker is associated with one or several commands. It sends a request to * the command. */ class Invoker { /** * @var Command */ private $onStart; /** * @var Command */ private $onFinish; /** * Initialize commands. */ public function setOnStart(Command $command): void { $this->onStart = $command; } public function setOnFinish(Command $command): void { $this->onFinish = $command; } /** * The Invoker does not depend on concrete command or receiver classes. The * Invoker passes a request to a receiver indirectly, by executing a * command. */ public function doSomethingImportant(): void { echo "Invoker: Does anybody want something done before I begin?\n"; if ($this->onStart instanceof Command) { $this->onStart->execute(); } echo "Invoker: ...doing something really important...\n"; echo "Invoker: Does anybody want something done after I finish?\n"; if ($this->onFinish instanceof Command) { $this->onFinish->execute(); } } } /** * The client code can parameterize an invoker with any commands. */ $invoker = new Invoker(); $invoker->setOnStart(new SimpleCommand("Say Hi!")); $receiver = new Receiver(); $invoker->setOnFinish(new ComplexCommand($receiver, "Send email", "Save report")); $invoker->doSomethingImportant();
Output.txt: 실행 결과
Invoker: Does anybody want something done before I begin? SimpleCommand: See, I can do simple things like printing (Say Hi!) Invoker: ...doing something really important... Invoker: Does anybody want something done after I finish? ComplexCommand: Complex stuff should be done by a receiver object. Receiver: Working on (Send email.) Receiver: Also working on (Save report.)
실제 사례 예시
이 예시에서 커맨드 패턴은 IMDB 웹사이트에 대한 웹 스크래핑 호출을 대기열에 넣고 하나씩 실행하는 데 사용됩니다. 대기열 자체는 데이터베이스에 보관되며 이 데이터베이스는 스크립트 실행 사이에 명령을 보존하는 것을 돕습니다.
index.php: 실제 사례 예시
<?php namespace RefactoringGuru\Command\RealWorld; /** * The Command interface declares the main execution method as well as several * helper methods for retrieving a command's metadata. */ interface Command { public function execute(): void; public function getId(): int; public function getStatus(): int; } /** * The base web scraping Command defines the basic downloading infrastructure, * common to all concrete web scraping commands. */ abstract class WebScrapingCommand implements Command { public $id; public $status = 0; /** * @var string URL for scraping. */ public $url; public function __construct(string $url) { $this->url = $url; } public function getId(): int { return $this->id; } public function getStatus(): int { return $this->status; } public function getURL(): string { return $this->url; } /** * Since the execution methods for all web scraping commands are very * similar, we can provide a default implementation and let subclasses * override them if needed. * * Psst! An observant reader may spot another behavioral pattern in action * here. */ public function execute(): void { $html = $this->download(); $this->parse($html); $this->complete(); } public function download(): string { $html = file_get_contents($this->getURL()); echo "WebScrapingCommand: Downloaded {$this->url}\n"; return $html; } abstract public function parse(string $html): void; public function complete(): void { $this->status = 1; Queue::get()->completeCommand($this); } } /** * The Concrete Command for scraping the list of movie genres. */ class IMDBGenresScrapingCommand extends WebScrapingCommand { public function __construct() { $this->url = "https://www.imdb.com/feature/genre/"; } /** * Extract all genres and their search URLs from the page: * https://www.imdb.com/feature/genre/ */ public function parse($html): void { preg_match_all("|href=\"(https://www.imdb.com/search/title\?genres=.*?)\"|", $html, $matches); echo "IMDBGenresScrapingCommand: Discovered " . count($matches[1]) . " genres.\n"; foreach ($matches[1] as $genre) { Queue::get()->add(new IMDBGenrePageScrapingCommand($genre)); } } } /** * The Concrete Command for scraping the list of movies in a specific genre. */ class IMDBGenrePageScrapingCommand extends WebScrapingCommand { private $page; public function __construct(string $url, int $page = 1) { parent::__construct($url); $this->page = $page; } public function getURL(): string { return $this->url . '?page=' . $this->page; } /** * Extract all movies from a page like this: * https://www.imdb.com/search/title?genres=sci-fi&explore=title_type,genres */ public function parse(string $html): void { preg_match_all("|href=\"(/title/.*?/)\?ref_=adv_li_tt\"|", $html, $matches); echo "IMDBGenrePageScrapingCommand: Discovered " . count($matches[1]) . " movies.\n"; foreach ($matches[1] as $moviePath) { $url = "https://www.imdb.com" . $moviePath; Queue::get()->add(new IMDBMovieScrapingCommand($url)); } // Parse the next page URL. if (preg_match("|Next »</a>|", $html)) { Queue::get()->add(new IMDBGenrePageScrapingCommand($this->url, $this->page + 1)); } } } /** * The Concrete Command for scraping the movie details. */ class IMDBMovieScrapingCommand extends WebScrapingCommand { /** * Get the movie info from a page like this: * https://www.imdb.com/title/tt4154756/ */ public function parse(string $html): void { if (preg_match("|<h1 itemprop=\"name\" class=\"\">(.*?)</h1>|", $html, $matches)) { $title = $matches[1]; } echo "IMDBMovieScrapingCommand: Parsed movie $title.\n"; } } /** * The Queue class acts as an Invoker. It stacks the command objects and * executes them one by one. If the script execution is suddenly terminated, the * queue and all its commands can easily be restored, and you won't need to * repeat all of the executed commands. * * Note that this is a very primitive implementation of the command queue, which * stores commands in a local SQLite database. There are dozens of robust queue * solution available for use in real apps. */ class Queue { private $db; public function __construct() { $this->db = new \SQLite3(__DIR__ . '/commands.sqlite', SQLITE3_OPEN_CREATE | SQLITE3_OPEN_READWRITE); $this->db->query('CREATE TABLE IF NOT EXISTS "commands" ( "id" INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL, "command" TEXT, "status" INTEGER )'); } public function isEmpty(): bool { $query = 'SELECT COUNT("id") FROM "commands" WHERE status = 0'; return $this->db->querySingle($query) === 0; } public function add(Command $command): void { $query = 'INSERT INTO commands (command, status) VALUES (:command, :status)'; $statement = $this->db->prepare($query); $statement->bindValue(':command', base64_encode(serialize($command))); $statement->bindValue(':status', $command->getStatus()); $statement->execute(); } public function getCommand(): Command { $query = 'SELECT * FROM "commands" WHERE "status" = 0 LIMIT 1'; $record = $this->db->querySingle($query, true); $command = unserialize(base64_decode($record["command"])); $command->id = $record['id']; return $command; } public function completeCommand(Command $command): void { $query = 'UPDATE commands SET status = :status WHERE id = :id'; $statement = $this->db->prepare($query); $statement->bindValue(':status', $command->getStatus()); $statement->bindValue(':id', $command->getId()); $statement->execute(); } public function work(): void { while (!$this->isEmpty()) { $command = $this->getCommand(); $command->execute(); } } /** * For our convenience, the Queue object is a Singleton. */ public static function get(): Queue { static $instance; if (!$instance) { $instance = new Queue(); } return $instance; } } /** * The client code. */ $queue = Queue::get(); if ($queue->isEmpty()) { $queue->add(new IMDBGenresScrapingCommand()); } $queue->work();
Output.txt: 실행 결과
WebScrapingCommand: Downloaded https://www.imdb.com/feature/genre/ IMDBGenresScrapingCommand: Discovered 14 genres. WebScrapingCommand: Downloaded https://www.imdb.com/search/title?genres=comedy IMDBGenrePageScrapingCommand: Discovered 50 movies. WebScrapingCommand: Downloaded https://www.imdb.com/search/title?genres=sci-fi IMDBGenrePageScrapingCommand: Discovered 50 movies. WebScrapingCommand: Downloaded https://www.imdb.com/search/title?genres=horror IMDBGenrePageScrapingCommand: Discovered 50 movies. WebScrapingCommand: Downloaded https://www.imdb.com/search/title?genres=romance IMDBGenrePageScrapingCommand: Discovered 50 movies. ...