Model Events (PSR-14)¶
Overview¶
Starting with Phalcon v6.0, model lifecycle events are dispatched as typed PSR-14 event objects in addition to the legacy string-based system. Each event is represented by a dedicated class under Phalcon\Db\Event, carrying a direct reference to the model that triggered it. This replaces the generic Phalcon\Events\Event object with strongly typed, purpose-specific events.
For the general PSR-14 events system overview, see PSR-14 Events.
NOTE
The legacy string-based model events (model:beforeCreate, etc.) continue to work. The PSR-14 events are dispatched in addition to the legacy events, so existing code is not affected. See Legacy Model Events for the old documentation.
Event Classes¶
Phalcon provides the following PSR-14 event classes for model lifecycle events. All extend Phalcon\Db\Event\AbstractModelEvent and implement Phalcon\Events\PsrEventInterface:
| Event Class | Lifecycle | Cancellable | Description |
|---|---|---|---|
Phalcon\Db\Event\AfterCreateEvent | Insert | No | Fired after creating a record |
Phalcon\Db\Event\AfterDeleteEvent | Delete | No | Fired after deleting a record |
Phalcon\Db\Event\AfterFetchEvent | Fetch | No | Fired after fetching a record |
Phalcon\Db\Event\AfterSaveEvent | Insert/Update | No | Fired after saving a record |
Phalcon\Db\Event\AfterUpdateEvent | Update | No | Fired after updating a record |
Phalcon\Db\Event\AfterValidationEvent | Insert/Update | Yes | Fired after validation completes |
Phalcon\Db\Event\AfterValidationOnCreateEvent | Insert | Yes | Fired after validation on create |
Phalcon\Db\Event\AfterValidationOnUpdateEvent | Update | Yes | Fired after validation on update |
Phalcon\Db\Event\BeforeCreateEvent | Insert | Yes | Fired before creating a record |
Phalcon\Db\Event\BeforeDeleteEvent | Delete | Yes | Fired before deleting a record |
Phalcon\Db\Event\BeforeSaveEvent | Insert/Update | Yes | Fired before saving a record |
Phalcon\Db\Event\BeforeUpdateEvent | Update | Yes | Fired before updating a record |
Phalcon\Db\Event\BeforeValidationEvent | Insert/Update | Yes | Fired before validation starts |
Phalcon\Db\Event\BeforeValidationOnCreateEvent | Insert | Yes | Fired before validation on create |
Phalcon\Db\Event\BeforeValidationOnUpdateEvent | Update | Yes | Fired before validation on update |
Phalcon\Db\Event\NotDeletedEvent | Delete | No | Fired when a delete fails |
Phalcon\Db\Event\NotSavedEvent | Insert/Update | No | Fired when a save fails |
Phalcon\Db\Event\OnValidationFailsEvent | Insert/Update | No | Fired when validation fails |
Phalcon\Db\Event\PrepareSaveEvent | Insert/Update | No | Fired before saving, allows data manipulation |
Phalcon\Db\Event\ValidationEvent | Insert/Update | Yes | Fired during validation |
AbstractModelEvent¶
All model event classes extend a common base:
<?php
namespace Phalcon\Db\Event;
use Phalcon\Events\PsrEventInterface;
use Phalcon\Mvc\Model;
abstract class AbstractModelEvent implements PsrEventInterface
{
public function __construct(public Model $model)
{
}
}
The $model property gives you direct, typed access to the model instance that triggered the event.
AbstractCancellableModelEvent¶
The 11 cancellable model events (those marked "Yes" in the Cancellable column above) extend AbstractCancellableModelEvent instead of AbstractModelEvent directly. This class implements PSR-14's StoppableEventInterface, giving listeners a standard way to cancel model operations:
<?php
namespace Phalcon\Db\Event;
use Psr\EventDispatcher\StoppableEventInterface;
abstract class AbstractCancellableModelEvent extends AbstractModelEvent implements StoppableEventInterface
{
private bool $cancelled = false;
public function cancel(): void
{
$this->cancelled = true;
}
public function isPropagationStopped(): bool
{
return $this->cancelled;
}
}
When a listener calls $event->cancel():
- No further listeners in the current
fireQueue()call are invoked - No further classes in the model hierarchy are dispatched to
- The
fireEventCancel()method returnsfalse, aborting the model operation (e.g. preventing a save or delete)
Concrete event classes¶
Each non-cancellable event class is a simple extension of the base:
<?php
namespace Phalcon\Db\Event;
class AfterCreateEvent extends AbstractModelEvent
{
}
class AfterDeleteEvent extends AbstractModelEvent
{
}
class PrepareSaveEvent extends AbstractModelEvent
{
}
// ... and so on for each non-cancellable event type
Cancellable events extend AbstractCancellableModelEvent:
<?php
namespace Phalcon\Db\Event;
class BeforeCreateEvent extends AbstractCancellableModelEvent
{
}
class BeforeDeleteEvent extends AbstractCancellableModelEvent
{
}
class ValidationEvent extends AbstractCancellableModelEvent
{
}
// ... and so on for each cancellable event type
How Events Are Dispatched¶
When a model triggers a lifecycle event (e.g. during save()), the following happens:
- The model's own event method is called if it exists (e.g.
$model->prepareSave()) - The
modelsEventFactoryservice creates the appropriate PSR-14 event object - For each class in the model's hierarchy (the model class, its parent classes, and its interfaces), the events manager dispatches:
- A wildcard event keyed by the class name (e.g.
MyApp\Models\Invoices) - A specific event keyed by class and event name (e.g.
MyApp\Models\Invoices:prepareSave) - The legacy
modelsManager->notifyEvent()is also called for backwards compatibility
This hierarchy dispatch means a single listener attached to a parent class or interface will fire for all models in that hierarchy.
ModelEventNameEnum¶
The Phalcon\Db\Event\ModelEventNameEnum enum maps string event names to their PSR-14 event classes:
<?php
namespace Phalcon\Db\Event;
enum ModelEventNameEnum: string
{
case AFTER_CREATE = 'afterCreate';
case AFTER_DELETE = 'afterDelete';
case AFTER_FETCH = 'afterFetch';
case AFTER_SAVE = 'afterSave';
case AFTER_UPDATE = 'afterUpdate';
case AFTER_VALIDATION = 'afterValidation';
case AFTER_VALIDATION_ON_CREATE = 'afterValidationOnCreate';
case AFTER_VALIDATION_ON_UPDATE = 'afterValidationOnUpdate';
case BEFORE_CREATE = 'beforeCreate';
case BEFORE_DELETE = 'beforeDelete';
case BEFORE_SAVE = 'beforeSave';
case BEFORE_UPDATE = 'beforeUpdate';
case BEFORE_VALIDATION = 'beforeValidation';
case BEFORE_VALIDATION_ON_CREATE = 'beforeValidationOnCreate';
case BEFORE_VALIDATION_ON_UPDATE = 'beforeValidationOnUpdate';
case NOT_DELETED = 'notDeleted';
case NOT_SAVED = 'notSaved';
case ON_VALIDATION_FAILS = 'onValidationFails';
case PREPARE_SAVE = 'prepareSave';
case VALIDATION = 'validation';
}
This enum is used internally by the Factory class and the events manager to resolve event names to classes and vice versa.
Event Factory¶
The Phalcon\Db\Event\Factory creates PSR-14 event objects from string event names. It is registered in the DI container as the modelsEventFactory service:
<?php
namespace Phalcon\Db\Event;
use Phalcon\Di\Di;
use Phalcon\Events\PsrEventInterface;
use Phalcon\Mvc\Model;
class Factory
{
public function __construct(protected Di $di)
{
}
public function create($eventName, Model $model): ?PsrEventInterface
{
try {
$className = ModelEventNameEnum::getEventClass($eventName);
return $this->di->get($className, [$model]);
} catch (UnknownEventTypeException $e) {
return null;
}
}
}
The factory returns null for unknown event names, allowing the legacy system to handle them gracefully.
Listening to Model Events¶
Using anonymous functions¶
The simplest way to listen for model events is with a closure. Attach it using the model class name combined with the event name:
<?php
use Phalcon\Events\Manager as EventsManager;
use Phalcon\Db\Event\AfterCreateEvent;
use MyApp\Models\Invoices;
$eventsManager = new EventsManager();
// Listen to afterCreate on Invoices specifically
$eventsManager->attach(
[Invoices::class, 'afterCreate'],
function (AfterCreateEvent $event) {
$invoice = $event->model;
echo 'Invoice created: ' . $invoice->inv_number;
}
);
$invoice = new Invoices();
$invoice->setEventsManager($eventsManager);
$invoice->inv_cst_id = 10;
$invoice->inv_title = 'Invoice for ACME Inc.';
$invoice->inv_total = 500.00;
$invoice->save(); // Triggers AfterCreateEvent
Using a listener class¶
Create a listener class with methods named after the events. The events manager will automatically call the matching method:
<?php
namespace MyApp\Listeners;
use Phalcon\Db\Event\PrepareSaveEvent;
use Phalcon\Db\Event\AfterCreateEvent;
use Phalcon\Db\Event\AfterDeleteEvent;
class InvoiceLifecycleListener
{
public function prepareSave(PrepareSaveEvent $event): void
{
$invoice = $event->model;
// Auto-generate invoice number before saving
if (empty($invoice->inv_number)) {
$invoice->inv_number = 'INV-' . date('YmdHis');
}
}
public function afterCreate(AfterCreateEvent $event): void
{
$invoice = $event->model;
// Send notification about new invoice
error_log('New invoice created: ' . $invoice->inv_number);
}
public function afterDelete(AfterDeleteEvent $event): void
{
$invoice = $event->model;
error_log('Invoice deleted: ' . $invoice->inv_number);
}
}
Attach the listener using the model's class name as a wildcard - the events manager calls the appropriate method for each event type:
<?php
use Phalcon\Events\Manager as EventsManager;
use MyApp\Models\Invoices;
use MyApp\Listeners\InvoiceLifecycleListener;
$eventsManager = new EventsManager();
// Wildcard: listen to ALL events on Invoices
$eventsManager->attach(
Invoices::class,
new InvoiceLifecycleListener()
);
$invoice = new Invoices();
$invoice->setEventsManager($eventsManager);
$invoice->inv_cst_id = 10;
$invoice->inv_title = 'Invoice for ACME Inc.';
$invoice->inv_total = 500.00;
$invoice->save();
// Calls: InvoiceLifecycleListener::prepareSave()
// Calls: InvoiceLifecycleListener::afterCreate()
Using __invoke¶
A listener class with __invoke receives every event dispatched to it:
<?php
namespace MyApp\Listeners;
use Phalcon\Db\Event\AbstractModelEvent;
class ModelEventLogger
{
public function __invoke(AbstractModelEvent $event): void
{
error_log(
sprintf(
'[%s] %s on %s',
date('Y-m-d H:i:s'),
$event::class,
get_class($event->model)
)
);
}
}
Cancelling Model Operations¶
Cancellable events implement Psr\EventDispatcher\StoppableEventInterface. A listener can call $event->cancel() to abort the model operation:
<?php
use Phalcon\Events\Manager as EventsManager;
use Phalcon\Db\Event\BeforeCreateEvent;
use MyApp\Models\Invoices;
$eventsManager = new EventsManager();
// Prevent creating invoices with zero total
$eventsManager->attach(
[Invoices::class, 'beforeCreate'],
function (BeforeCreateEvent $event) {
if ($event->model->inv_total <= 0) {
// Cancel the create operation
$event->cancel();
}
}
);
$invoice = new Invoices();
$invoice->setEventsManager($eventsManager);
$invoice->inv_cst_id = 10;
$invoice->inv_title = 'Invalid Invoice';
$invoice->inv_total = 0;
$invoice->save(); // Will NOT create the record — the beforeCreate was cancelled
NOTE
Only the 11 cancellable events (listed with "Yes" in the Cancellable column) support cancel(). Non-cancellable events like AfterCreateEvent or PrepareSaveEvent do not implement StoppableEventInterface.
Interface-Based Event Listening¶
The class hierarchy dispatch enables a powerful pattern: listen to events on an interface so your listener fires for all models that implement it.
Define an interface¶
<?php
namespace MyApp\Models;
interface AuditableInterface
{
public function getFieldsToAudit(): ?array;
}
Implement on your models¶
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model implements AuditableInterface
{
public int $inv_id;
public int $inv_cst_id;
public string $inv_title;
public float $inv_total;
public function getFieldsToAudit(): ?array
{
return ['inv_total', 'inv_title'];
}
}
class Users extends Model implements AuditableInterface
{
public int $id;
public string $email;
public bool $is_active;
public function getFieldsToAudit(): ?array
{
return ['email', 'is_active'];
}
}
Register one listener for all auditable models¶
<?php
use Phalcon\Events\Manager as EventsManager;
use Phalcon\Db\Event\AfterSaveEvent;
use MyApp\Models\AuditableInterface;
$eventsManager = new EventsManager();
// This fires for BOTH Invoices and Users (and any other AuditableInterface)
$eventsManager->attach(
[AuditableInterface::class, 'afterSave'],
function (AfterSaveEvent $event) {
$model = $event->model;
if ($model instanceof AuditableInterface) {
$fields = $model->getFieldsToAudit();
// Write audit log for the changed fields...
error_log('Audit: ' . get_class($model) . ' saved. Tracking: ' . implode(', ', $fields));
}
}
);
DI Container Setup¶
The modelsEventFactory service is automatically registered when using Phalcon\Di\FactoryDefault. If you are setting up the DI container manually, register it like this:
<?php
use Phalcon\Di\Di;
use Phalcon\Db\Event\Factory as ModelEventFactory;
$container = new Di();
$container->setShared(
'modelsEventFactory',
function () use ($container) {
return new ModelEventFactory($container);
}
);
Full Database Example¶
Here is a complete example showing the PSR-14 event system with database models:
<?php
use Phalcon\Di\FactoryDefault;
use Phalcon\Events\Manager as EventsManager;
use Phalcon\Db\Adapter\Pdo\Mysql;
use Phalcon\Db\Event\PrepareSaveEvent;
use Phalcon\Db\Event\AfterCreateEvent;
use Phalcon\Db\Event\AfterSaveEvent;
use Phalcon\Db\Event\NotSavedEvent;
// Setup DI
$container = new FactoryDefault();
$container->set('db', function () {
return new Mysql([
'host' => 'localhost',
'username' => 'root',
'password' => 'secret',
'dbname' => 'myapp',
]);
});
// Create events manager
$eventsManager = new EventsManager();
// Listener class for Invoices
class InvoiceEventHandler
{
public function prepareSave(PrepareSaveEvent $event): void
{
$invoice = $event->model;
echo get_class($invoice) . " is being prepared for save\n";
}
public function afterCreate(AfterCreateEvent $event): void
{
$invoice = $event->model;
echo "Invoice #{$invoice->inv_id} created\n";
}
public function afterSave(AfterSaveEvent $event): void
{
$invoice = $event->model;
echo "Invoice #{$invoice->inv_id} saved\n";
}
}
// Attach handler to Invoice model events
$eventsManager->attach(
\MyApp\Models\Invoices::class,
new InvoiceEventHandler()
);
// Also listen to save failures with a closure
$eventsManager->attach(
[\MyApp\Models\Invoices::class, 'notSaved'],
function (NotSavedEvent $event) {
$invoice = $event->model;
error_log('Failed to save invoice: ' . implode(', ', $invoice->getMessages()));
}
);
// Use the model
$invoice = new \MyApp\Models\Invoices();
$invoice->setEventsManager($eventsManager);
$invoice->inv_cst_id = 10;
$invoice->inv_title = 'Invoice for ACME Inc.';
$invoice->inv_total = 1500.00;
$invoice->save();
// Output:
// MyApp\Models\Invoices is being prepared for save
// Invoice #1 created
// Invoice #1 saved
Logging SQL Statements¶
The database layer events work the same way with the PSR-14 system. You can still use the events manager to log SQL statements:
<?php
use Phalcon\Db\Adapter\Pdo\Mysql;
use Phalcon\Di\FactoryDefault;
use Phalcon\Events\Manager;
use Phalcon\Logger\Logger;
use Phalcon\Logger\Adapter\Stream;
$container = new FactoryDefault();
$container->set(
'db',
function () {
$eventsManager = new Manager();
$adapter = new Stream('/storage/logs/db.log');
$logger = new Logger(
'messages',
[
'main' => $adapter,
]
);
$eventsManager->attach(
'db:beforeQuery',
function ($event, $connection) use ($logger) {
$logger->info(
$connection->getSQLStatement()
);
}
);
$connection = new Mysql(
[
'host' => 'localhost',
'username' => 'root',
'password' => 'secret',
'dbname' => 'phalcon',
]
);
$connection->setEventsManager($eventsManager);
return $connection;
}
);
NOTE
Database layer events (db:beforeQuery, db:afterQuery, etc.) currently still use the legacy string-based system. The PSR-14 typed event objects are available for model lifecycle events (afterCreate, afterDelete, prepareSave, etc.).