I know this is an old question, but I'd like to point out that the issue directly stems from an incorrect premise. That is, the aggregate roots we are meant to assume exist are simply incorrect.
There is only one aggregate root in the system you have described: Customer. Both an Order and Feedback, while they may be aggregates in their own right, are dependent on the Customer for existence so are not themselves aggregate roots. The logic you provide in your feedback constructor seems to indicate that an Order MUST have a customerId and Feedback MUST also be related to a Customer. This makes sense. How can an Order or Feedback not be related to a Customer? Additionally, Supplier seems to logically be related to Order (so would be inside this aggregate).
With the above in mind, all of the information you want is already available in the Customer aggregate root and it becomes clear you are enforcing your rules in the wrong place. Constructors are terrible places to enforce business rules and should be avoided at all costs. This is what it should look like (Note: I'm not going to include constructors for Customer and Order because Factories should probably be used. Also not showing all interface methods).
Interfaces, explained below
public function getId() : int;
interface IUser extends ICustomer
public function getUsername() : string;
public function getPassword() : string;
public function changeUsername( string $new ) : void;
public function resetPassword( string $new ) : void;
interface IReviewer extends ICustomer
public function provideFeedback( IOrder $order, string $content ) : void;
interface IBuyer extends ICustomer
public function placeOrder( IOrder $order ) : void;
public function getCustomerId() : int;
public function addFeedback( string $content ) : void;
public function addContent( string $content ) : void;
public function isValidContent( string $content ) : void;
class Customer implements IReviewer, IBuyer
protected $orders = ;
public function provideFeedback( IOrder $order, string $content ) : void
if( $order->getCustomerId() !== $this->getId() )
throw new \InvalidArgumentException('Customers can only provide feedback on their own orders');
$order->addFeedback( $content );
class Order implements IOrder
protected $feedbacks = ;
public function addFeedback( string $content ) : void
if( false === $this->supplier->isOperating() )
throw new \Exception('Feedback can only be added to orders if the supplier is still operating.');
// could be any IFeedback
$feedback = new Feedback( $this );
$feedback->addContent( $content );
$this->feedbacks = $feedback;
class Feedback implements IFeedback
public function __construct( IOrder $order )
// we don't carry our business rules in constructors
$this->order = $order;
public function addContent( string $content ) : void
if( false === $this->isValidContent($content) )
throw new \Exception("Content contains offensive language.");
$this->content = $content;
Okay. Let's break this down a little bit. The first thing you will notice is how much more declarative this model is. Everything is an action, it becomes clear WHERE business rules should apply. The design above doesn't just "do" the right thing, it "says" the right thing.
What would lead anyone to assume rules are being executed in the following line?
// this is a BAD place for rules to execute
$feedback = new Feedback( $id, $customerId, $order, $supplier, $content);
Second, you can see that all of the logic pertaining to validating business rules is carried out as closely as possible to the models to which they pertain. In your example, the constructor (a single method) is performing multiple validations against different models. That breaks SOLID design. Where would we add a check to make sure that the Feedback content doesn't contain bad words? Another check in the constructor? What if different kinds of Feedback need different content checks? Ugly.
Third, looking at the interfaces, you can see there are natural places to extend/modify the rules through composition. For example, different kinds of orders can have different rules regarding when feedback can be provided. Order can also provide different kinds of feedback, which in turn can have different rules for validation.
You can also see a bunch of ICustomer* interfaces. These are used to compose the Customer aggregate we need here (probably not just called Customer). The reason for this is simple. It's VERY likely that a Customer is a HUGE aggregate root that spreads out all over your domain/DB. By using interfaces, we can decompose that one aggregate (which is likely too large to load) into multiple aggregate roots that only provide certain actions (like ordering or providing feedback). You can see the aggregate in my implementation can BOTH place orders AND provide feedback, but cannot be used to reset a password or change a username.
So the answer to your question is that aggregates should validate themselves. If they can't you likely have a deficient model.