Command

The Command pattern turns a request — "do this action" — into a standalone object. Once an action is an object instead of a direct method call, it can be passed around, stored in a queue, logged for auditing, or even reversed later, instead of only ever happening immediately and being forgotten.

The Problem It Solves

Normally, the code that decides an action should happen calls straight into the code that performs it:

<?php

class UserController {
    public function handleDeleteRequest(string $userId): void {
        // Directly coupled to UserRepository — hard to queue, log, or undo
        (new UserRepository())->delete($userId);
    }
}

That's fine until you need any of the following: run the action later instead of right now (a job queue), keep a record of exactly what happened and in what order (an audit log), or let the user reverse the action (undo). None of that is possible when "trigger" and "perform" are the same line of code — there's no object representing the action itself that could be held onto, inspected, or reversed.

What Command Actually Does

Command wraps every action in its own object behind a shared interface:

  • A common interface declares an execute() method (and optionally an undo() method).
  • Each concrete command class holds whatever data and dependencies it needs to perform its one action.
  • An Invoker holds and runs command objects by calling execute() — without knowing or caring what any command actually does.

A Realistic Worked Example: User Management Commands

The shared interface, including an undo() method so commands can be reversed:

<?php

interface CommandInterface {
    public function execute(): void;
    public function undo(): void;
}

Each command class holds exactly the data and dependencies it needs to do — and undo — its one job:

<?php

class CreateUserCommand implements CommandInterface {
    private UserRepository $users;
    private array $data;
    private ?string $createdId = null;

    public function __construct(UserRepository $users, array $data) {
        $this->users = $users;
        $this->data = $data;
    }

    public function execute(): void {
        $this->createdId = $this->users->create($this->data);
    }

    public function undo(): void {
        if ($this->createdId !== null) {
            $this->users->delete($this->createdId);
        }
    }
}

class DeleteUserCommand implements CommandInterface {
    private UserRepository $users;
    private string $userId;
    private ?array $deletedUser = null;

    public function __construct(UserRepository $users, string $userId) {
        $this->users = $users;
        $this->userId = $userId;
    }

    public function execute(): void {
        // Keep a copy so undo() can recreate the user if needed
        $this->deletedUser = $this->users->find($this->userId);
        $this->users->delete($this->userId);
    }

    public function undo(): void {
        if ($this->deletedUser !== null) {
            $this->users->create($this->deletedUser);
        }
    }
}

And an Invoker that runs commands (and can undo the last one) without any idea what they do:

<?php

class Invoker {
    private array $history = [];

    public function run(CommandInterface $command): void {
        $command->execute();
        $this->history[] = $command;
    }

    public function undoLast(): void {
        $command = array_pop($this->history);
        $command?->undo();
    }
}

// Usage — the invoker never references CreateUserCommand or DeleteUserCommand by name
$invoker = new Invoker();
$invoker->run(new CreateUserCommand($users, ['name' => 'Aisha']));
$invoker->undoLast(); // Removes the user that was just created
Why the Invoker matters: Invoker::run() only ever calls execute() on an interface type. It can be handed a CreateUserCommand, a DeleteUserCommand, or any future command class — its code never needs to change to support a new action.

Real-World PHP: Queued Jobs

Laravel's queued jobs are a real-world cousin of this pattern: a job class bundles up an action plus the data it needs (much like CreateUserCommand bundling UserRepository and $data), gets serialized, and is executed later by a queue worker that — just like our Invoker — only knows how to call the job's handle() method, not what any specific job actually does.

When to Reach for Command

  • Undo/redo systems — anywhere users need to reverse an action.
  • Job queues — deferring work to run later, possibly on a different process.
  • Macro recording — capturing a sequence of actions as objects so they can be replayed together.
  • Audit logging — command objects are natural, inspectable records of "what happened."
Up next: Iterator covers how to walk through a custom collection's elements with a plain foreach loop, without exposing how that collection stores its data internally.