Events
Craft has all kinds of events you can use to customize how core features work, or connect built-in processes to new functionality. Events are a Yii concept (opens new window), and are used extensively throughout its architecture. In cases where Yii components are extended for internal use (like craft\base\Model (opens new window)), Craft provides additional events to expose a greater customization surface for developers.
See Using Events in a Custom Module (opens new window) for an end-to-end tutorial on wiring up your first event handler in a module.
This page covers customizing Craft’s native behavior by registering event handlers in your plugin, and how to implement your own events that other developers can take advantage of.
# Anatomy of an Event
We use “event” to describe the name and sender that uniquely identify it, as well as the actual yii\base\Event (opens new window) instance that is emitted.
# Sender
The class that emits an event is considered its sender. The sender is always available via the sender
property of an event object, and inherits from yii\base\Component (opens new window). Events are emitted by calling a component’s trigger()
method, which automatically sets up the event-sender relationship. You can see an example of this in the custom events section.
# Name
Combined with the sender, an event’s name identifies what a given handler is listening for. Event names are unique among those on the same class. Typically, you will access event names via constants on the sender class (like craft\services\Dashboard::EVENT_REGISTER_WIDGET_TYPES (opens new window)), instead of using the underlying string (registerWidgetTypes
). Using event constants also allows your IDE to give you suggestions for events, and provide feedback when referencing a non-existent one—they’re also part of our formal deprecation process, receiving docblock tags and upgrade instructions.
# Event Object
Events are emitted as a yii\base\Event (opens new window) instance, or a subclass thereof. Craft has a number of specific event classes (in the craft\events\
namespace) that act as specialized containers for data that a handler might be interested in.
# Handled
Each time an event is emitted, a new event object is created. That object is passed as an argument to each registered handler, until a handler sets the handled
property to true
—subsequent handlers are then skipped.
Plugins should generally avoid flagging events handled
unless they expect to have exclusive control over another event property. Craft events that use the EVENT_SET_*
, EVENT_DEFINE_*
, or EVENT_REGISTER_*
naming convention are good examples of this—situations where a handler might need to authoritatively replace an event’s value to guarantee some behavior of the extension.
# Cancelable Events
Craft extends the “handled” concept via craft\events\CancelableEvent (opens new window). In addition to handled
, handlers can set the event’s isValid
property to false
to signal that the sender should halt further processing. If you want to prevent other handlers from overriding your isValid
setting, also set handled
to true
.
The isValid
property is not necessarily related to model validation. A handful of Yii events use an isValid
property to mean a variety of things, but Craft standardizes the behavior via CancelableEvent (opens new window), using it exclusively as a means to halt a built-in procedure (say, to prevent a user from being activated).
Preventing something from happening in this way is not equivalent to throwing an exception or attempting to end the request from a handler. If an event is cancelable, Craft expects to be able to react to its cancellation—including, potentially, releasing locks or rolling back database transactions.
# Discovering Events
Every event emitted by an application is logged and can be reviewed with the Yii debug toolbar.
But what about all the events that weren’t emitted during a given request? Craft and Yii use the same convention for events: every event name is defined exactly one time, as a class constant starting with EVENT_
.
# Code Diving
The best way to discover an event is to get familiar with a known procedure by stepping through it with a tool like xdebug and looking for event constants within or adjacent to the logical flow, or on classes that logic is part of. Alternatively, setting a breakpoint on the low-level yii\base\Event::trigger() (opens new window) or yii\base\Component::trigger() (opens new window) methods will allow you to inspect every event emitted during a request—and view the current call’s trace.
A fresh Craft install can easily emit 100 or more events per request, so using your IDE’s conditional breakpoint may be necessary to cut down on noise!
Directly searching the Craft source is a great way to learn about Craft-specific events—but it will miss a host of events emitted by Yii, as well as those that are inherited from parent classes. The event browser and generator below enumerates all of those events, even if they’re technically defined by a parent.
# Inherited Events
Events provided by Craft (those defined on classes in the craft\*
namespace) represent just the tip of the iceberg. Many classes inherit events from their parents, including classes defined by Yii—let’s use craft\elements\Entry (opens new window) as an example:
- Only two events exist directly on the
Entry
class:
EVENT_DEFINE_ENTRY_TYPES
(opens new window)EVENT_DEFINE_PARENT_SELECTION_CRITERIA
(opens new window)
- Its parent class, craft\base\Element (opens new window) defines 38 more:
Element
extends craft\base\Component (opens new window), which doesn’t contribute any—but its parent class craft\base\Model (opens new window) defines five more:
- From here, we jump into yii\base\Model (opens new window), which defines two more:
- The final two classes in the chain of inheritance (yii\base\Component (opens new window) and yii\base\BaseObject (opens new window)) don’t implement any events, themselves.
This one class contains many more events (47, at the time of writing) than is immediately evident. This also represents only the events that would designate an entry as its sender—entries (and elements or models) are passed to a variety of other services, which emit their own events. Another 33 events are emitted by craft\services\Elements (opens new window), alone!
# Handling Events
Events can be attached with different scopes, depending on your needs.
# Class-Level Events
In this mode, a handler is registered for all occurrences of an event on a class. Class-level handlers are attached using the static yii\base\Event::on() (opens new window):
use Craft;
use yii\base\Event;
use craft\services\Users;
use craft\events\UserEvent;
Event::on(
Users::class,
Users::EVENT_AFTER_ACTIVATE_USER,
function(UserEvent $e) {
$message = Craft::$app->getMailer()
->composeFromKey('custom.welcome')
->setTo($e->user);
// ...
$message->send();
}
);
Let’s look at the required arguments:
- The fully-qualified class name that we expect to emit an event.
- The event name we want to register the handler for. This should always be a class constant).
- The handler.
Class-level events are almost always attached from a plugin’s init()
method—but be aware that Craft’s bootstrapping process may trigger some events before handlers can be registered! We advise developers defer as much of their public plugins’ initialization as possible by wrapping it in a call to craft\base\ApplicationTrait::onInit() (opens new window)—this gives other extensions an opportunity to register handlers prior to them actually being triggered.
# Instance-Level Events
If you’re only concerned about events emitted by a single instance of a class, handlers can be directly attached to any subclass of yii\base\Component (opens new window) (effectively every service and model in Craft, including Craft itself).
This example attaches a handler via the singleton yii\base\Application (opens new window) instance that is available as Craft::$app
:
use Craft;
use yii\base\Application;
Craft::$app->on(
Application::EVENT_AFTER_REQUEST,
function(Event $e) {
// ...
}
);
In a different scenario, we may want to attach a handler on any object that passes through some method in our extension:
namespace mynamespace\myplugin\services;
use craft\base\Element;
use craft\events\ModelEvent;
use yii\base\Component;
class TrackChanges extends Component
{
public function watch(Element $element): void
{
$element->on(
Element::EVENT_AFTER_SAVE,
function(ModelEvent $e) {
$originalAttributes = $e->data->originalAttributes;
// Perform some comparison of new/old attributes...
},
[
'originalAttributes' => $element->getAttributes(),
],
);
}
}
This is a much less common pattern for dealing with built-in components, and will usually involve an initial class-level event handler to discover objects as they’re created, anyway.
In most cases, it’s best to set a class-level listener and check in your handler whether the event (or its sender) is relevant. If you know every instance of a class will require an event, you can also attach them via behaviors.
This listener makes use of the fourth $data
argument, which allows you to capture data as part of the handler. This is available on the event object when the handler is invoked, under a data
property. Be mindful of the volume of data you’re capturing at this stage, as it may prevent PHP from freeing memory.
# Handler Signature
Handlers must be a valid callable (opens new window), and should accept a single argument that matches the expected event type. Our examples so far have used either the generic yii\base\Event (opens new window), or a type hint of craft\events\UserEvent (opens new window).
In addition to closures, As of PHP 8.1, you may also use the native callable syntax. Our previous “welcome email” example would end up looking something like this:
Event::on(
Users::class,
Users::EVENT_AFTER_ACTIVATE_USER,
[MyService::class, 'handleUserActivation']
);
# Return Values
Return values from handlers are ignored—instead, Craft expects that the handler modifies properties on the event object (or its sender). You may explicitly declare a void
return type on handlers as a means to identify misuse within your own plugin, but the signature is not enforced.
# Cloning Handlers
Instance-level event handlers are not copied to new instances when using PHP’s clone()
. If you want to guarantee that your handlers survive this process, create a behavior and attach that—behaviors do get copied thanks to craft\base\CloneFixTrait (opens new window), and any event handlers declared by yii\base\Behavior::events() (opens new window) are re-installed.
Instead of maintaining a behavior for a single handler, you can use the built-in craft\behaviors\EventBehavior (opens new window) 4.5.0+ as a proxy for registering handlers:
use craft\behaviors\EventBehavior;
use craft\elements\db\ElementQuery;
use craft\elements\Entry;
use craft\events\CancelableEvent;
$query = Entry::find();
$query->attachBehavior('myBehavior', new EventBehavior([
ElementQuery::EVENT_AFTER_PREPARE => function(CancelableEvent $event, ElementQuery $query) {
// ...
},
], true));
The second argument to the EventBehavior
constructor tells the behavior to mimic craft\base\Event::once() (opens new window) and will only invoke your handler once. All handlers are treated the same way, but you may mix one-time and continuous handlers by attaching multiple EventBehavior
instances.
# Event Code Generator
Select an event for more detail and a code snippet.
# Common Event Flows
# Adding Validation Rules
Models in Craft (including elements) implement validation rules (opens new window) via a defineRules()
method. When any model is validated, its rules()
method is called, which in turn calls defineRules()
, then emits a craft\events\DefineRulesEvent (opens new window) with those rules set on a rules
property.
A handler that registers additional rules would look something like this:
use yii\base\Event;
use craft\elements\User;
use craft\events\DefineRulesEvent;
Event::on(
User::class,
User::EVENT_DEFINE_RULES,
function(DefineRulesEvent $e) {
// Enforce password minimum length:
$e->rules[] = [['newPassword'], 'string', 'min' => 16];
}
);
Note that the handler is pushing a new rule onto the event’s rules
property, rather than returning a value.
You can add validation rules to custom fields in the same handler:
// Reject a value if it looks like spam:
$e->rules[] = [
['field:myCustomFieldHandle'],
'match',
'pattern' => '/extended warranty/i',
'not' => true,
];
Building your own field type? It should provide validation rules via getElementValidationRules()
.
This process is the same for any class that extends craft\base\Model (opens new window)—the EVENT_DEFINE_RULES
event is available on any subclass!
# Saving Entries
While every element in Craft CMS has a common set of events your custom code can subscribe to, the entry-saving workflow is one of the most common and complex.
See Handling Entry Saves (opens new window) for more on entry-specific concepts.
Generally, entries progress through the following order of operations:
- Pre-flight checks that trigger
EVENT_BEFORE_SAVE
. - Validation that triggers
EVENT_BEFORE_VALIDATE
andEVENT_AFTER_VALIDATE
. - Saving for the initial site that triggers
EVENT_AFTER_SAVE
. - Propagating non-translatable changes to the entry’s other sites, which repeats steps 1-3 for each site before triggering
EVENT_AFTER_PROPAGATE
.
# Adding and Modifying Search Keywords
If you’d like to extend system components to add your own searchable custom attributes, you can hook into the EVENT_REGISTER_SEARCHABLE_ATTRIBUTES
(opens new window) event.
Here, we’re making the myCustomAttribute
property searchable for Commerce orders:
use craft\base\Element;
use craft\commerce\elements\Order;
use craft\events\RegisterElementSearchableAttributesEvent;
use yii\base\Event;
Event::on(
Order::class,
Element::EVENT_REGISTER_SEARCHABLE_ATTRIBUTES,
function(RegisterElementSearchableAttributesEvent $event) {
$event->attributes[] = 'billingCountry';
// ...
}
);
A “searchable attribute” doesn’t need to correspond to a real property, so long as you later handle the craft\base\Element::EVENT_DEFINE_KEYWORDS (opens new window) event:
use craft\base\Element;
use craft\commerce\elements\Order;
use craft\events\DefineAttributeKeywordsEvent;
use yii\base\Event;
Event::on(
Order::class,
Element::EVENT_DEFINE_KEYWORDS,
function(DefineAttributeKeywordsEvent $event) {
if ($event->attribute !== 'billingCountry') {
return;
}
$billingAddress = $event->sender->getBillingAddress();
$country = Craft::$app->getAddresses()
->getCountryRepository()
->get($billingAddress->countryCode);
$event->keywords = $country->name;
}
);
The same process can be repeated for each searchable attribute you wish to define—or it can all be packaged into a behavior!
# Custom Events
Events are only necessary (or practical) for your extension if you expect other developers to want to customize or react to its behavior. Private plugins and modules generally do not need to emit events, as you (the author) already have complete control over its internal logic!
# The “Evented” Mindset
As you design your extension, it may still be worthwhile to consider some of the patterns that events fit into. Let’s use a newsletter sign-up plugin as an example. Our plugin will need…
- …a controller to receive input from a front-end form;
- …a model to hold the contact’s details;
- …a service to communicate with a third-party API;
Where do events fit in this process? Craft will automatically emit events before our controller action is run, when an instance of our model is created, before and after our model is validated, after a response is sent… and potentially dozens more places in the normal request-response lifecycle.
Our “API” service is still a bit of a black box, though. Let’s give developers access to our request and response objects by creating a pair of events; one emitted just before our request to the API (with a reference to the contact being subscribed), and another emitted when we’re confident a contact has been subscribed (with references to the contact and the raw response data from the API).
namespace mynamespace\newsletter\events;
use craft\events\CancelableEvent;
use mynamespace\newsletter\models\Contact;
class SubscribeEvent extends CancelableEvent
{
public Contact $contact;
}