文章来源refactoringguru.cn/design-patt…
PHP 命令模式讲解和代码示例
命令是一种行为设计模式, 它可将请求或简单操作转换为一个对象。
此类转换让你能够延迟进行或远程执行请求, 还可将其放入队列中。
复杂度:******
流行度:******
使用示例: 命令模式在 Java 代码中很常见。 它被用于对任务进行排序、 记录任务执行历史以及执行 “撤销” 操作。
识别方法: 命令模式可以通过抽象或接口类型 (发送者) 中的行为方法来识别, 该类型调用另一个不同的抽象或接口类型 (接收者) 实现中的方法, 该实现则是在创建时由命令模式的实现封装。 命令类通常仅限于一些特殊行为。
真实世界示例
在本例中, 命令模式被用于对 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.
...