Skip to content

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).

Register a PSR-14 dispatcher with Propulsion::setEventDispatcher(), typically right after Propulsion::init():

<?php
use 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).

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:

HookEventStoppable
preSave()PreSaveEventYes
postSave()PostSaveEventNo
preInsert()PreInsertEventYes
postInsert()PostInsertEventNo
preUpdate()PreUpdateEventYes
postUpdate()PostUpdateEventNo
preDelete()PreDeleteEventYes
postDelete()PostDeleteEventNo

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.

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 effect

Post* events are not stoppable — the operation has already happened by the time they’re dispatched.

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:

MethodPre-event (stoppable)Post-event
update()PreBulkUpdateEventPostBulkUpdateEvent
delete() / deleteAll()PreBulkDeleteEventPostBulkDeleteEvent

Both event families expose getCriteria(): ModelCriteria (the query/criteria describing the affected rows) and getConnection(): ?PropulsionPDO.

  • PreBulkUpdateEvent additionally carries the column-name-to-value map passed to update() via getValues(). A listener can call setValues() to replace that map before the update proceeds — the mutated array is what actually gets applied to the matched rows.
  • PostBulkUpdateEvent exposes getAffectedRowCount() and getValues() (the map actually applied, post any listener mutation).
  • PostBulkDeleteEvent exposes getAffectedRowCount().
  • Calling stopPropagation() on either Pre* event vetoes the whole bulk operation — update()/delete()/deleteAll() return 0 without 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.

UnitOfWork::flush() dispatches a third pair, for the whole batch rather than any single object:

EventStoppableExposes
PreFlushEventyesgetUnitOfWork(): UnitOfWork, getConnection(): PropulsionPDO
PostFlushEventnogetUnitOfWork(), 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().

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.

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.