Команда — это поведенческий паттерн, позволяющий заворачивать запросы или простые операции в отдельные объекты.
Это позволяет откладывать выполнение команд, выстраивать их в очереди, а также хранить историю и делать отмену.
Концептуальный пример
Этот пример показывает структуру паттерна Команда , а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.
index.php: Пример структуры паттерна
<?php
namespace RefactoringGuru\Command\Conceptual;
/**
* Интерфейс Команды объявляет метод для выполнения команд.
*/
interface Command
{
public function execute(): void;
}
/**
* Некоторые команды способны выполнять простые операции самостоятельно.
*/
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";
}
}
/**
* Но есть и команды, которые делегируют более сложные операции другим объектам,
* называемым «получателями».
*/
class ComplexCommand implements Command
{
/**
* @var Receiver
*/
private $receiver;
/**
* Данные о контексте, необходимые для запуска методов получателя.
*/
private $a;
private $b;
/**
* Сложные команды могут принимать один или несколько объектов-получателей
* вместе с любыми данными о контексте через конструктор.
*/
public function __construct(Receiver $receiver, string $a, string $b)
{
$this->receiver = $receiver;
$this->a = $a;
$this->b = $b;
}
/**
* Команды могут делегировать выполнение любым методам получателя.
*/
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);
}
}
/**
* Классы Получателей содержат некую важную бизнес-логику. Они умеют выполнять
* все виды операций, связанных с выполнением запроса. Фактически, любой класс
* может выступать Получателем.
*/
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";
}
}
/**
* Отправитель связан с одной или несколькими командами. Он отправляет запрос
* команде.
*/
class Invoker
{
/**
* @var Command
*/
private $onStart;
/**
* @var Command
*/
private $onFinish;
/**
* Инициализация команд.
*/
public function setOnStart(Command $command): void
{
$this->onStart = $command;
}
public function setOnFinish(Command $command): void
{
$this->onFinish = $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();
}
}
}
/**
* Клиентский код может параметризовать отправителя любыми командами.
*/
$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;
/**
* Интерфейс Команды объявляет основной метод выполнения, а также несколько
* вспомогательных методов для получения метаданных команды.
*/
interface Command
{
public function execute(): void;
public function getId(): int;
public function getStatus(): int;
}
/**
* Базовая Команда скрейпинга устанавливает базовую инфраструктуру загрузки,
* общую для всех конкретных команд скрейпинга.
*/
abstract class WebScrapingCommand implements Command
{
public $id;
public $status = 0;
/**
* @var string URL для скрейпинга.
*/
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;
}
/**
* Поскольку методы выполнения для всех команд скрейпинга очень похожи, мы
* можем предоставить реализацию по умолчанию, позволив подклассам
* переопределить её при необходимости.
*
* Шш! Наблюдательный читатель может обнаружить здесь другой поведенческий
* паттерн в действии.
*/
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);
}
}
/**
* Конкретная Команда для извлечения списка жанров фильма.
*/
class IMDBGenresScrapingCommand extends WebScrapingCommand
{
public function __construct()
{
$this->url = "https://www.imdb.com/feature/genre/";
}
/**
* Извлечение всех жанров и их поисковых URL со страницы:
* 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));
}
}
}
/**
* Конкретная Команда для извлечения списка фильмов определённого жанра.
*/
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;
}
/**
* Извлечение всех фильмов со страницы вроде этой:
* 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));
}
// Извлечение URL следующей страницы.
if (preg_match("|Next »</a>|", $html)) {
Queue::get()->add(new IMDBGenrePageScrapingCommand($this->url, $this->page + 1));
}
}
}
/**
* Конкретная Команда для извлечения подробных сведений о фильме.
*/
class IMDBMovieScrapingCommand extends WebScrapingCommand
{
/**
* Получить информацию о фильме с подобной страницы:
* 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";
}
}
/**
* Класс Очередь действует как Отправитель. Он складывает объекты команд в стек
* и выполняет их поочерёдно. Если выполнение скрипта внезапно завершится,
* очередь и все её команды можно будет легко восстановить, и вам не придётся
* повторять все выполненные команды.
*
* Обратите внимание, что это очень примитивная реализация очереди команд,
* которая хранит команды в локальной базе данных SQLite. Существуют десятки
* надёжных реализаций очереди, доступных для использования в реальных
* приложениях.
*/
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();
}
}
/**
* Для удобства объект Очереди является Одиночкой.
*/
public static function get(): Queue
{
static $instance;
if (!$instance) {
$instance = new Queue();
}
return $instance;
}
}
/**
* Клиентский код.
*/
$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.
...