Model events
Propulsion dispatches model lifecycle events through PSR-14 (Psr\EventDispatcher\EventDispatcherInterface), mirroring the existing PSR-3 logging facade: Propulsion ships no concrete event dispatcher implementation of its own; you bring one (Symfony’s EventDispatcher, league/event, or anything else implementing the interface).
Registering a dispatcher
Section titled “Registering a dispatcher”Register a PSR-14 dispatcher with Propulsion::setEventDispatcher(), typically right after Propulsion::init():
<?phpuse Propulsion\Propulsion;use Symfony\Component\EventDispatcher\EventDispatcher;
Propulsion::init('/path/to/runtime-conf.php');
$dispatcher = new EventDispatcher();$dispatcher->addListener(\Propulsion\Event\PostSaveEvent::class, function ($event) { // ...});Propulsion::setEventDispatcher($dispatcher);Check whether one is registered with Propulsion::hasEventDispatcher(), and retrieve it with Propulsion::eventDispatcher() (returns null if none was set).
Per-object lifecycle events
Section titled “Per-object lifecycle events”Propulsion\OM\BaseObject’s preSave()/postSave()/preInsert()/postInsert()/preUpdate()/postUpdate()/preDelete()/postDelete() hooks — the same methods described in Active Record reference: Lifecycle events — each dispatch a corresponding event from the Propulsion\Event namespace:
| Hook | Event | Stoppable |
|---|---|---|
preSave() | PreSaveEvent | Yes |
postSave() | PostSaveEvent | No |
preInsert() | PreInsertEvent | Yes |
postInsert() | PostInsertEvent | No |
preUpdate() | PreUpdateEvent | Yes |
postUpdate() | PostUpdateEvent | No |
preDelete() | PreDeleteEvent | Yes |
postDelete() | PostDeleteEvent | No |
Every event exposes getObject(): BaseObject — the actual instance being saved/deleted, not a copy, so mutating it from a listener is visible to the rest of the save()/delete() call — and getConnection(): ?PropulsionPDO, the connection the operation is running on, if one was available.
Vetoing an operation
Section titled “Vetoing an operation”The Pre* events implement PSR-14’s StoppableEventInterface. A listener that calls $event->stopPropagation() vetoes the operation, exactly like an overridden preSave()/preInsert()/preUpdate()/preDelete() method returning false always has:
<?php$dispatcher->addListener(\Propulsion\Event\PreDeleteEvent::class, function (\Propulsion\Event\PreDeleteEvent $event) { if ($event->getObject()->isProtected()) { $event->stopPropagation(); }});
$book->delete(); // no-op, the transaction never opens, delete() returns without effectPost* events are not stoppable — the operation has already happened by the time they’re dispatched.
Bulk update/delete events
Section titled “Bulk update/delete events”Propulsion\Query\ModelCriteria::update(), delete(), and deleteAll() dispatch a separate family of events from the same namespace, since a bulk operation runs a single SQL statement over a whole result set rather than acting on one loaded BaseObject:
| Method | Pre-event (stoppable) | Post-event |
|---|---|---|
update() | PreBulkUpdateEvent | PostBulkUpdateEvent |
delete() / deleteAll() | PreBulkDeleteEvent | PostBulkDeleteEvent |
Both event families expose getCriteria(): ModelCriteria (the query/criteria describing the affected rows) and getConnection(): ?PropulsionPDO.
PreBulkUpdateEventadditionally carries the column-name-to-value map passed toupdate()viagetValues(). A listener can callsetValues()to replace that map before the update proceeds — the mutated array is what actually gets applied to the matched rows.PostBulkUpdateEventexposesgetAffectedRowCount()andgetValues()(the map actually applied, post any listener mutation).PostBulkDeleteEventexposesgetAffectedRowCount().- Calling
stopPropagation()on eitherPre*event vetoes the whole bulk operation —update()/delete()/deleteAll()return0without touching the database.
These bulk events are dispatched independently of the per-object events above: a bulk update()/delete() does not trigger preSave()/postSave()/preDelete()/postDelete() on each affected row unless update() is called with $forceIndividualSaves = true, in which case it goes through the normal per-object save() path (and its events) for every row instead of a single bulk statement. See ModelCriteria & Query reference for the surrounding API.
Flush events
Section titled “Flush events”UnitOfWork::flush() dispatches a third pair, for the whole batch rather than any single object:
| Event | Stoppable | Exposes |
|---|---|---|
PreFlushEvent | yes | getUnitOfWork(): UnitOfWork, getConnection(): PropulsionPDO |
PostFlushEvent | no | getUnitOfWork(), getConnection(), getAffectedRows(): int |
PreFlushEvent is dispatched before the transaction opens; a listener calling stopPropagation() aborts the entire flush, and flush() returns 0 with nothing persisted. PostFlushEvent is dispatched after a successful commit.
The per-object events above still fire for each entity written during a flush, since flush() calls each entity’s own save()/delete().
Listener exceptions
Section titled “Listener exceptions”Exceptions thrown by a listener are not caught by Propulsion::dispatch() — they propagate out of whichever hook triggered the dispatch, and out of the save()/delete()/update() call that invoked the hook.
Generated save()/delete() code catches \Throwable around the hook calls specifically so that a listener exception of any type still rolls back the transaction save()/delete() opened before propagating. You don’t need to make listeners throw PropulsionException to get correct rollback behaviour.
What didn’t change
Section titled “What didn’t change”The pre-existing convention of overriding preSave()/postSave()/etc. directly in a stub Active Record class (see Active Record reference: Lifecycle events) still works exactly as before, and is unaffected by whether an event dispatcher is registered — the base hook methods now additionally dispatch an event, but they still return the same boolean veto contract they always have.