Skip to content

Extension Events Since 3.5.0

Extensions can subscribe to events fired by Total CMS after core operations complete. Events are synchronous and fire after the operation succeeds. If a listener throws an exception, it is caught and logged without affecting the core operation or other listeners.

public function register(ExtensionContext $context): void
{
$context->addEventListener('object.created', function (array $payload): void {
// Handle the event
}, priority: 0);
}

The priority parameter controls execution order (lower numbers run first). Default is 0.

Fired after a new object is saved to a collection.

KeyTypeDescription
collectionstringCollection name
idstringObject ID
$context->addEventListener('object.created', function (array $payload): void {
$collection = $payload['collection'];
$objectId = $payload['id'];
// e.g., send a webhook notification
});

Fired after an existing object is updated.

KeyTypeDescription
collectionstringCollection name
idstringObject ID
$context->addEventListener('object.updated', function (array $payload): void {
// e.g., clear a CDN cache for this object
});

Fired after an object is deleted from a collection.

KeyTypeDescription
collectionstringCollection name
idstringObject ID
$context->addEventListener('object.deleted', function (array $payload): void {
// e.g., clean up related data in your extension
});

Fired after a new collection is created.

KeyTypeDescription
collectionstringCollection ID
$context->addEventListener('collection.created', function (array $payload): void {
// e.g., set up default content for a new collection
});

Fired after a collection’s settings are updated.

KeyTypeDescription
collectionstringCollection ID
$context->addEventListener('collection.updated', function (array $payload): void {
// e.g., react to collection configuration changes
});

Fired after a collection is deleted.

KeyTypeDescription
collectionstringCollection ID
$context->addEventListener('collection.deleted', function (array $payload): void {
// e.g., clean up extension data related to this collection
});

Fired per object when an importer creates a new object. Replaces object.created for the duration of an import — during import, object.created is suppressed for the importing collection so listeners can distinguish import-time writes from user-driven ones.

KeyTypeDescription
collectionstringCollection ID
idstringObject ID
objectObjectDataNewly created object
$context->addEventListener('import.created', function (array $payload): void {
// e.g., index the newly imported object in an external search service
});

Fired by: ObjectImporter (used by CSV, JSON, RSS, WordPress, URL, Alloy, Total CMS 1 imports), JumpStartImporter, UrlImporter.

Fired per object when an importer updates an existing object. Replaces object.updated during import (same suppression model as import.created).

KeyTypeDescription
collectionstringCollection ID
idstringObject ID
objectObjectDataUpdated object
previous?ObjectDataObject state before the update (when available)
$context->addEventListener('import.updated', function (array $payload): void {
// e.g., diff payload['object'] against payload['previous']
});

Fired by: ObjectImporter (CSV/JSON update mode), DeckJsonImporter, DeckCsvImporter.

Fired ONCE per batch import (CSV, JSON, or URL) after all objects in the batch are processed. Triggers a single index rebuild and auto-resumes the object.* event suppression for the collection.

KeyTypeDescription
collectionstringCollection ID
countintNumber of objects imported
createdstring[]IDs of newly created objects
updatedstring[]IDs of updated objects
$context->addEventListener('import.completed', function (array $payload): void {
$collection = $payload['collection'];
$count = $payload['count'];
// e.g., send a notification that import is done
// Act on specific updated objects
foreach ($payload['updated'] as $id) {
// e.g., send an email to updated members
}
});

Why subscribe to import.* instead of object.*? When a 10,000-row CSV imports, object.created would fire 10,000 times — but it doesn’t, because the dispatcher suspends it. Subscribe to import.created if you want per-object import notifications; subscribe to import.completed if you only care about the batch summary; subscribe to both if you need both.

Fired after a schema is created or updated.

KeyTypeDescription
schemastringSchema ID
$context->addEventListener('schema.saved', function (array $payload): void {
// e.g., regenerate a search index
});

Fired after a schema is deleted.

KeyTypeDescription
schemastringSchema ID
$context->addEventListener('schema.deleted', function (array $payload): void {
// e.g., remove cached data for this schema
});

Fired after a Builder template (.twig file) is written via TemplateSaver. Powers the Builder live-reload feature internally — extensions can listen too, e.g., to re-warm a template cache or trigger a downstream rebuild.

KeyTypeDescription
idstringTemplate id without folder prefix (e.g., about, partials/header)
folderstring|nullOptional sub-folder (pages, layouts, etc.) or null for root
pathstringFull path including folder (e.g., pages/about)
$context->addEventListener('template.saved', function (array $payload): void {
// e.g., flush a template-derived cache key
});

Fired after a user successfully logs in.

KeyTypeDescription
userstringUser ID or email
$context->addEventListener('user.login', function (array $payload): void {
// e.g., track login activity
});

Fired after a user logs out.

KeyTypeDescription
userstringUser ID or email
$context->addEventListener('user.logout', function (array $payload): void {
// e.g., clean up temporary data
});

Fired after an extension is enabled.

KeyTypeDescription
idstringExtension ID (e.g. vendor/name)

Fired after an extension is disabled.

KeyTypeDescription
idstringExtension ID (e.g. vendor/name)

Fired after development mode is enabled.

KeyTypeDescription
durationintDuration in seconds
$context->addEventListener('devmode.enabled', function (array $payload): void {
// e.g., enable verbose logging in your extension
});

Fired after development mode is disabled.

The payload array is empty.

$context->addEventListener('devmode.disabled', function (array $payload): void {
// e.g., disable debug features in your extension
});

Fired after all caches are cleared.

KeyTypeDescription
successboolWhether all caches cleared successfully

The payload also includes per-service results (e.g. filesystem, redis, apcu).

$context->addEventListener('cache.cleared', function (array $payload): void {
// e.g., clear your extension's own cache
});

Each listener is wrapped in a try/catch. If your listener throws:

  1. The exception is logged to logs/extensions.log
  2. Other listeners for the same event continue executing
  3. The core operation that triggered the event is not affected (it already completed)

This means you should not rely on events for critical side effects that must succeed. Events are best for notifications, caching, analytics, and other non-critical reactions to content changes.

Listeners with lower priority numbers execute first:

// Runs first
$context->addEventListener('object.created', $listenerA, priority: 10);
// Runs second
$context->addEventListener('object.created', $listenerB, priority: 20);

If two listeners have the same priority, they execute in the order they were registered.

Sending Notifications from Event Listeners

Section titled “Sending Notifications from Event Listeners”

Extensions can use events to trigger notifications automatically when content changes. The built-in PushoverService and EmailService are available via the DI container in the boot() phase.

use TotalCMS\Domain\Notification\Service\PushoverService;
public function boot(ExtensionContext $context): void
{
$pushover = $context->get(PushoverService::class);
$context->addEventListener('object.created', function (array $payload) use ($pushover): void {
$pushover->send(
message: "New {$payload['collection']} object: {$payload['id']}",
title: 'Content Created',
);
});
}
use TotalCMS\Domain\Mailer\Service\EmailService;
public function boot(ExtensionContext $context): void
{
$mailer = $context->get(EmailService::class);
$context->addEventListener('import.completed', function (array $payload) use ($mailer): void {
$mailer->sendEmail('import-notification', [
'collection' => $payload['collection'],
'count' => $payload['count'],
]);
});
}

These listeners run after the core operation completes. If sending fails, the exception is caught and logged without affecting the content operation.