Skip to Content
Course content

138: The Command Pattern in PHP

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this all the time when developers first encounter the Command Pattern: they look at the boilerplate and think, "Why on earth would I wrap a simple method call inside a whole new class? I can just call $light->turnOn() directly. This is just adding three files to do something that takes one line of code."

It's Just a Wrapper for a Method Call

If your only goal is to make something happen right now, then yes, the Command Pattern is overkill. If you just want to turn on a light, calling the method is the right move. But the misconception is thinking that the pattern is about execution. It's actually about encapsulation.

Imagine you're building a smart home system. If you hard-code every action, your "Remote Control" class becomes a nightmare. It has to know exactly which devices exist and which methods they have. If you add a new brand of smart bulb that uses $bulb->activate() instead of $light->turnOn(), you have to go back and change the logic in your remote control. You've tightly coupled the "trigger" to the "implementation."

Turning an Action into a First-Class Object

The shift in thinking is this: stop thinking about "calling a function" and start thinking about "creating a request object." When you turn a request into an object, that object can be stored in a queue, passed as a parameter, or pushed onto a stack. It becomes a "thing" you can manipulate.

// First, we define the contract. Every command must be executable.
interface Command {
    public function execute(): void;
    public function undo(): void;
}

// The Receiver: The actual object that knows how to do the work.
class Light {
    public function turnOn() { echo "The light is ON\n"; }
    public function turnOff() { echo "The light is OFF\n"; }
}

// The Concrete Command: This bridges the gap.
class LightOnCommand implements Command {
    private $light;

    public function __construct(Light $light) {
        $this->light = $light;
    }

    public function execute(): void {
        $this->light->turnOn();
    }

    public function undo(): void {
        $this->light->turnOff();
    }
}

Now, notice that the LightOnCommand doesn't just "do" the action; it knows how to reverse it. Because the command is an object, it can hold the state necessary to undo itself. You can't "undo" a method call once it's gone, but you can call undo() on a command object.

Decoupling the Trigger from the Logic

This is where the "Invoker" comes in. The Invoker doesn't know what the command does; it just knows that it can execute it. This allows you to create complex behaviors—like macros or undo history—without the Invoker ever knowing the details of your business logic.

class RemoteControl {
    private $history = [];

    public function submit(Command $command) {
        $command->execute();
        $this->history[] = $command;
    }

    public function pressUndo() {
        if (!empty($this->history)) {
            $command = array_pop($this->history);
            $command->undo();
        }
    }
}

// Usage
$livingRoomLight = new Light();
$turnOn = new LightOnCommand($livingRoomLight);
$remote = new RemoteControl();

$remote->submit($turnOn); // Output: The light is ON
$remote->pressUndo();     // Output: The light is OFF

I'll be honest: in a tiny project, this feels like a lot of typing. But when you're building something like a text editor with an undo/redo stack, or a job queue where commands are serialized and pushed to Redis to be processed later, this pattern is a lifesaver. You've effectively decoupled the when (the remote button) from the how (the light's internal API).




📋 Practical Task

Build a Transactional File System Command Manager

You need to implement a basic "File Manager" that supports creating files and deleting files, but with a critical requirement: every action must be undoable.

Your Task:

  • Create a Command interface with execute() and undo() methods.
  • Create a FileSystem class (the Receiver) with two methods: createFile($name) and deleteFile($name). These should just print a message to the screen (e.g., "File 'test.txt' created").
  • Implement a CreateFileCommand and a DeleteFileCommand.
    • The CreateFileCommand should call createFile() on execution and deleteFile() on undo.
    • The DeleteFileCommand should call deleteFile() on execution and createFile() on undo.
  • Implement a CommandHistory class (the Invoker) that maintains a stack of executed commands and has a method undoLastAction().

Test Case: Create a file named "document.txt", then create a file named "notes.txt". Call undoLastAction() once, and verify that "notes.txt" is deleted. Call it again, and verify that "document.txt" is deleted.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.