PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Wzorzec Command – undo, CommandBus, makra, integracja z kolejką Magento 2

by Henryk Tews / wtorek, 09 marca 2021 / Opublikowano w Wzorce projektowe


Wzorzec Command zamienia żądanie wykonania operacji w samodzielny obiekt. To pozwala kolejkować operacje, logować je, cofać (undo), a nawet budować makra z sekwencji komend. W ekosystemie PHP pattern Command znajdziesz w Symfony Console, w Magento jako podstawę systemu kolejkowania i w każdym systemie który obsługuje historię operacji.

Idea wzorca

Bez wzorca Command logika wywołania operacji jest bezpośrednio w wywołującym. Trudno to kolejkować, logować czy cofać:

<?php

// Bez Command - wywołujący wie za dużo
class OrderController
{
    public function cancel(int $orderId): void
    {
        $order = $this->orderRepository->getById($orderId);
        $order->setStatus('cancelled');
        $this->orderRepository->save($order);
        $this->emailService->sendCancellationEmail($order);
        $this->inventoryService->restoreStock($order);
        $this->logger->info('Order cancelled', ['id' => $orderId]);
        // ...i jeszcze 5 innych kroków
    }
}

Command przenosi całą tę logikę do obiektu komendy:

Implementacja wzorca Command

<?php

declare(strict_types=1);

// Interfejs komendy - jedna metoda execute()
interface CommandInterface
{
    public function execute(): void;
}

// Interfejs odwracalnej komendy
interface ReversibleCommandInterface extends CommandInterface
{
    public function undo(): void;
}
<?php

declare(strict_types=1);

// Konkretna komenda - enkapsuluje całą operację anulowania zamówienia
class CancelOrderCommand implements ReversibleCommandInterface
{
    private string $previousStatus = '';

    public function __construct(
        private int $orderId,
        private OrderRepositoryInterface $orderRepository,
        private EmailServiceInterface $emailService,
        private InventoryServiceInterface $inventoryService,
        private \Psr\Log\LoggerInterface $logger
    ) {}

    public function execute(): void
    {
        $order = $this->orderRepository->getById($this->orderId);

        // Zapamiętaj poprzedni stan dla undo()
        $this->previousStatus = $order->getStatus();

        $order->setStatus('cancelled');
        $this->orderRepository->save($order);
        $this->inventoryService->restoreStock($order);
        $this->emailService->sendCancellationEmail($order);

        $this->logger->info('Order cancelled', ['order_id' => $this->orderId]);
    }

    public function undo(): void
    {
        if (empty($this->previousStatus)) {
            throw new \LogicException('Cannot undo - command was not executed');
        }

        $order = $this->orderRepository->getById($this->orderId);
        $order->setStatus($this->previousStatus);
        $this->orderRepository->save($order);

        $this->logger->info('Order cancellation undone', ['order_id' => $this->orderId]);
    }
}

Command Invoker – historia i undo

<?php

declare(strict_types=1);

// Invoker - zarządza wykonywaniem i historią komend
class CommandBus
{
    /** @var ReversibleCommandInterface[] */
    private array $history = [];

    public function execute(CommandInterface $command): void
    {
        $command->execute();

        if ($command instanceof ReversibleCommandInterface) {
            $this->history[] = $command;
        }
    }

    public function undo(): void
    {
        if (empty($this->history)) {
            throw new \UnderflowException('No commands to undo');
        }

        $command = array_pop($this->history);
        $command->undo();
    }

    public function undoAll(): void
    {
        while (!empty($this->history)) {
            $this->undo();
        }
    }
}

// Użycie
$bus = new CommandBus();

$bus->execute(new CancelOrderCommand(42, $orderRepo, $emailService, $inventoryService, $logger));
$bus->execute(new CancelOrderCommand(43, $orderRepo, $emailService, $inventoryService, $logger));

// Cofnij ostatnią operację
$bus->undo(); // przywraca zamówienie 43

// Cofnij wszystko
$bus->undoAll();

Command z kolejkowaniem

Command naturalnie integruje się z systemami kolejkowania – obiekt komendy serializujesz i wysyłasz do queue, consumer deserializuje i wywołuje execute():

<?php

declare(strict_types=1);

// Komenda do kolejkowania - musi być serializowalna (tylko dane, nie serwisy)
class SendOrderConfirmationCommand implements CommandInterface
{
    public function __construct(
        public readonly int $orderId,
        public readonly string $customerEmail,
        public readonly string $locale
    ) {}

    public function execute(): void
    {
        // W kontekście konsumera zależności wstrzykuje DI
        // Tu tylko logika bez serwisów - serwisy wstrzykuje consumer
    }
}

// Consumer kolejki - wstrzykuje zależności i wykonuje komendę
class OrderConfirmationConsumer
{
    public function __construct(
        private EmailServiceInterface $emailService,
        private TemplateRenderer $renderer
    ) {}

    public function process(SendOrderConfirmationCommand $command): void
    {
        $template = $this->renderer->render(
            'order_confirmation',
            ['order_id' => $command->orderId, 'locale' => $command->locale]
        );

        $this->emailService->send($command->customerEmail, $template);
    }
}

Makra – sekwencje komend

<?php

// Makro - komenda złożona z wielu komend
class BatchCancelOrdersCommand implements CommandInterface
{
    /** @var CommandInterface[] */
    private array $commands = [];

    public function addCommand(CommandInterface $command): self
    {
        $this->commands[] = $command;
        return $this;
    }

    public function execute(): void
    {
        foreach ($this->commands as $command) {
            $command->execute();
        }
    }
}

// Użycie - anuluj wiele zamówień jako jedną operację
$batch = new BatchCancelOrdersCommand();

foreach ([42, 43, 44] as $orderId) {
    $batch->addCommand(
        new CancelOrderCommand($orderId, $orderRepo, $emailService, $inventoryService, $logger)
    );
}

$bus->execute($batch);

Command w Magento 2

Magento 2 używa wzorca Command w kilku miejscach. System komend CLI (bin/magento) to klasyczny Command z Symfony Console. Bardziej interesujący przykład to moduł Magento_SalesRule, gdzie każda reguła cenowa implementuje własną logikę obliczania rabatu jako oddzielna klasa – to wariant Command gdzie każdy „handler” enkapsuluje jedną regułę.

Możesz też zbudować własny Command Bus w Magento integrując się z systemem kolejkowania z poprzednich wpisów:

<?php

namespace Vendor\Module\CommandBus;

use Magento\Framework\MessageQueue\PublisherInterface;

class AsyncCommandBus
{
    private const TOPIC = 'vendor.module.command';

    public function __construct(
        private PublisherInterface $publisher
    ) {}

    // Wyślij komendę do asynchronicznego wykonania przez consumer
    public function dispatch(object $command): void
    {
        $this->publisher->publish(self::TOPIC, $command);
    }
}

Podsumowanie

Wzorzec Command błyszczy gdy operacje muszą być kolejkowane, logowane lub odwracane. Przeniesienie logiki z kontrolera do obiektu komendy upraszcza testy (testujesz komendę w izolacji), umożliwia ponowne użycie (ta sama komenda z controllera i z CLI) i otwiera drzwi do zaawansowanych funkcji jak undo czy makra. W kontekście Magento 2 warto myśleć o Command gdy implementujesz operacje biznesowe które lądują w kolejce – Message Queue i Command to naturalna para.

About Henryk Tews

Co możesz przeczytać następne

Wzorzec Template Method – szkielet algorytmu, hooks, abstract vs hook, porównanie ze Strategy
Wzorzec Repository – interfejs, implementacja, SearchCriteria, testowanie z mockiem
Wzorzec Observer w PHP i system zdarzeń Magento 2
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Zawsze aktywne
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}