“Send the confirmation in the background” arrives as a requirement long after the code that sends it has settled into a controller method, wired to a request and a session. If the operation is already an object carrying its own arguments, moving it off the request is a change to who calls the handler and to nothing else.
class ConfirmOrder // the arguments, and nothing else
{
public $orderId;
public $notifyBy;
public function __construct($orderId, $notifyBy)
{
$this->orderId = $orderId;
$this->notifyBy = $notifyBy;
}
}
class ConfirmOrderHandler // the behaviour, and its dependencies
{
public function handle(ConfirmOrder $command)
{
$order = $this->orders->find($command->orderId);
$this->mailer->send(new Confirmation($order), $command->notifyBy);
$this->stock->commit($order);
}
}
// in the controller, today
$this->bus->execute(new ConfirmOrder($id, 'email'));
// and later, with no change to the handler at all
$this->queue->push('ConfirmOrderHandler', serialize(new ConfirmOrder($id, 'email')));
The split that makes it work is that the command holds data and the handler holds dependencies. A command carrying a Mailer cannot be serialised onto a queue; one carrying an order id and a string can be, and can also be logged, replayed and diffed against the one that failed. That constraint is the whole discipline — if the operation needs the current user, the user id goes in the command, not the session. The cost is two classes where there was a method, which is far too much ceremony for something that will never leave the request. The test for whether it is worth it: could this plausibly be triggered by a console command, a webhook, or a retry? If not, leave it in the controller.