Anorm Another ORM logo Anorm Another ORM

Lifecycle hooks

Anorm exposes a single lifecycle hook, ChangeListenerInterface, for code that needs to know what changed on every successful DataMapper::write().

When to use it

Registering a listener

use Anorm\DataMapper;
use Anorm\Lifecycle\ChangeListenerInterface;
use Anorm\Model;

class AuditListener implements ChangeListenerInterface
{
    public function onWrite(Model $model, array $diff, bool $isInsert): void
    {
        if ($isInsert) {
            error_log('Inserted ' . get_class($model) . ' #' . $model->id);
            return;
        }
        foreach ($diff as $property => $change) {
            error_log(sprintf(
                '%s#%d.%s: %s -> %s',
                get_class($model), $model->id, $property,
                var_export($change['from'], true),
                var_export($change['to'], true)
            ));
        }
    }
}

DataMapper::setChangeListener(new AuditListener());

setChangeListener is static. Pass null to remove the listener (e.g. in test tearDown).

What the listener receives

Excluding fields from the diff

By default, diff reports every mapped property except:

To exclude additional properties (timestamps, audit columns, etc.), set DataMapper::$infrastructureProperties:

$mapper = DataMapper::createByClass($pdo, $model);
$mapper->infrastructureProperties = ['dtc', 'dtu', 'uc', 'uu'];

Snapshot lifecycle

Model::$_lastSnapshot is the per-model record of “values as last seen in the database.” It is populated at the end of DataMapper::readArray() and refreshed at the end of every successful write(). It is null until the first read while a listener is registered.

Important: snapshot capture is gated on setChangeListener being non-null. Register the listener at boot, before any reads of models that will later be written. A model read before the listener was registered will be treated as an INSERT on its next write (isInsert=true, diff=[]) — which is harmless but incorrect for change tracking.

Re-entrancy

A listener may call DataMapper::write() — for example, to persist a follow-up record on a different table in response to a change. Nested writes commit their SQL and refresh their own $_lastSnapshot, but do not re-invoke the listener. Anorm assumes a single in-flight listener and suppresses recursive notifications for any depth.

Listener exceptions

Anorm wraps the listener call in try/catch. Any exception thrown by the listener is logged via error_log and swallowed; the write itself succeeds. Listener faults must never break writes.

If you want strict behaviour, your listener can catch and re-throw a wrapper type — but most consumers prefer fire-and-forget.

Object equality

For object-valued properties, diff calls equals($other) if defined, otherwise isSame($other), otherwise PHP’s loose == (which compares all properties recursively for property-bag value objects). It does not fall back to serialize. Implement equals() on value objects whose semantic equality differs from property equality.