Aggregate boundaries are transaction boundaries

The rule that makes aggregates useful is that one transaction touches one aggregate. Everything else follows: what belongs inside, what is referenced by id, and where consistency is immediate rather than eventual.

final class Order            // aggregate root
{
    private $lines;          // inside: changed together, one transaction
    private $customerId;     // outside: referenced by id, not held

    public function addLine(Sku $sku, int $qty): void
    {
        // invariants across lines can be enforced here, synchronously
    }
}

If two things must always be consistent with each other, they are one aggregate. If they may briefly disagree, they are two, and the link is an id plus an event. Holding a reference to another aggregate object rather than its id is the usual mistake — it invites a transaction spanning both, and the invariant it appears to protect was never actually enforceable.