Domain events belong to the aggregate that raised them

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.