Our experience with Domain Events
Domain-Driven Design background
There are a series of Domain Model patterns that describe objects and objects group built with Domain-Driven Design. Aggregates describe cohesive object graph with a single point of entry, called root: the internal objects of the aggregate cannot be persistently references from the outside.
The domain classes whose instances are inside aggregates are subdivided into Entities and Value Objects: the former have a lifecycle (like a Post or a User), while the latter are just values with methods, equivalent to Strings and other domain concepts.
A prerequisite of these patterns is the immutability of Value Objects, which can then be shared between aggregates, just like String instances can be in many languages. Value Objects such as numbers and colors are modified by calling a method on them that return a new instance: every change to their state should produce a new Value Object.
Repositories are collection of aggregates: they model operations such as finding an aggregate or persisting a new one.
A great departure of modern DDD from the entity/relationship modelling everyone knows is the duplication of data between aggregates to support new scenarios: it's possible some field or object is repeated in different aggregates. When there is an update to an aggregate, it's not necessarily atomically reflected to the other copies of its data.
I'll refer to writing calls for generality, to indicate the Command side of the Command Query sepration, which corresponds to everything that causes a change in state in the domain objects (in opposition to the reading side).
Events as mail messages
Thus it has become common to copy data between objects in different aggregates: for example, think of a Document and Invoice object that share the same start/finish date interval. Traditionally this duplication is dealt with by extracting a common object, mapped to a common row in the database, with a name invented on the spot.
Domain events are an alternative that allows for duplicating these data: they reflect changes happened in a single aggregate, and are sent to other aggregates so that they can update themselves. Technically speaking, domain events are Plain Old $YourLanguage Objects, containing the modified data but not related to the ORM like the main domain objects.
Domain events are handy for modelling "when" rules that should always be respected no matter who is writing to an aggregate; moreover, their handling can take place in the same transaction or even in a new one.
My skeptic view of events was that it can be unclear which events are communicated between objects. After a while, I accepted that unit tests tell us that; moreover, communicating with events is a further level of abstraction which is unnecessary in simple domains but just a giant Observer pattern in others.
The underlying idea is that no matter who applies a command or modifies a domain object, we already configured the event handling mechanism so that consistency across aggregates is reached according to our policy defined in the event handlers (which may be immediate consistency, or eventual one. Or it may result in sending a mail to a human asking him to review the changes: whatever you want.)
The only alternative to propagate changes between aggregates would be to have many collaborators passed to the various Repositories, but this solution couples the aggregates with each other in many way, while with events you're forced to define one-way messages. The event generator does not make any assumption about who will listen to the event and if it will be listened to at all: events are a point of decoupling like interface are for object collaboration.
And it's not that we call static methods by passing a string. We have a clear contract, a DomainEvents static class, and we publish interesting events (like CreatedCar or UpdatedVoyagePlan) as plain old domain object which contain all the information about the update, often even composing the relevant domain object.
Udi Dahan discourages the reference to domain objects, and consider events just special Value Objects; indeed as our solution matures we are moving towards simpler objects. This choice may force us to consider just what needs to be inserted in the message instead of a full reference (where and if serialization is used to transmit the event, it's simpler to use a Value Object in fact). Moreover, it avoids possible further accidental writing calls to the domain object originating the event.
In the application layer
Events are published by calling a static domain class: as a result event launchers cannot be decoupled from the event (as in Udi Dahan's approach).
We launch events from the Repository after an update has been performed, either by choosing an event class directly (in case of an update or creation) or by collecting the events from a queue on the relevant domain object, usually the root of the aggregate. This was a nice idea from a colleague of mine that let us decouple at least Entities from the DomainEvents static class.
For now we do not have the requirement to decouple the handling of events from the transaction, so the application layer (which is over the domain layer) open and commits/aborts a transaction, while reconstituting an aggregate, doing some "writing" work (updating it or executing a Command) and saving it. The save triggers the event launch, which may trigger work on other aggregates through the configured handler: in case of an error the whole transaction is aborted, ensuring immediate consistency.
So we aren't getting the scaling advantages of deferred handling (we're not interested in that for now), but the simplicity of communicating with events while writing code.
This a PHP-specific section: however, domain events are an approach typical of Java or .NET enterprise applications.
We use PHP classes (or interfaces) for routing the events with instanceof; PHP is a shared nothing environment, so event configuration is done now on a per-action basis to avoid having to create all the objects handling events on each request.
However, we want to move the configuration to the application level, with some lazy-loading: for example, configuring lazy event handlers as methods on Factory objects that create the real handler and return it along with the name of the method to call. All communication between aggregates happen in a single process and a single address space (for now), so we don't use a bit of the decoupling properties of events.
We map Value Objects into the relational database either as on the parent entity's table (decomposing their fields onto the entity) or as row of their own table. In any case, we have to ensure immutability via encapsulation and only assignign to $this->anyField into the constructor.
Our standard pattern is to define setters as new self($this->field1, ..., $nwFieldValue, ..., $this->fieldN); where N is a small number of fields. We map all domain object with the Hibernate-equivalent Doctrine 2. We are investigating how to deal with orphaned Value Objects, which are not reached anymore by any other entity.