Command es un patrón de diseño de comportamiento que convierte solicitudes u operaciones simples en objetos.
La conversión permite la ejecución diferida de comandos, el almacenamiento del historial de comandos, etc.
Ejemplo conceptual
Este ejemplo ilustra la estructura del patrón de diseño Command y se centra en las siguientes preguntas:
¿De qué clases se compone?
¿Qué papeles juegan esas clases?
¿De qué forma se relacionan los elementos del patrón?
Después de conocer la estructura del patrón, será más fácil comprender el siguiente ejemplo basado en un caso de uso real de PHP.
index.php: Ejemplo conceptual
<?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: Resultado de la ejecución
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.)
Ejemplo del mundo real
En este ejemplo, el patrón Command se utiliza para poner en cola llamadas de web scraping al sitio web de IMDB y ejecutarlas una a una. La propia cola se mantiene en una base de datos para ayudar a preservar los comandos entre lanzamientos del script .
index.php: Ejemplo del mundo real
<?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: Resultado de la ejecución
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.
...