Publishing an event from a service layer means the aggregate can change state without the event being raised, because the two are in different places and only convention connects them.
final class Order
{
private $events = [];
public function ship(Carrier $carrier): void
{
$this->status = 'shipped';
$this->events[] = new OrderShipped($this->id, $carrier->code());
}
public function releaseEvents(): array
{
$events = $this->events;
$this->events = [];
return $events;
}
}
The aggregate records rather than publishes, and the repository releases the events after a successful save — so an event cannot escape for a transaction that rolled back. That ordering is the whole reason to do it this way. Publishing from inside the method, before the commit, is the version that eventually sends a shipping notification for an order that was never saved.