The Themes module fires action hooks at six points in a theme's lifecycle — two on activation, two on installation, and two on update. Hooks use the artisanpack-ui/hooks package (doAction() / addAction()).
Added in 2.0.0.
Activation hooks
ThemeManager::activateTheme($slug) fires:
theme.activating
- Fired: before the active-theme setting changes.
- Arguments:
(string $slug, ThemeContract $theme) - Listener may throw: yes — a thrown exception short-circuits activation and the prior theme stays active.
- Typical uses: validate the incoming theme against app-side preconditions, clear theme-scoped caches, write an audit log.
theme.activated
- Fired: after the active-theme setting is updated and view paths are re-registered.
- Arguments:
(string $slug, ThemeContract $theme) - Listener may throw: yes, but the change is already committed — throwing rolls back nothing.
- Typical uses: warm caches that depend on the active theme, send a "theme switched" notification, fire a webhook.
Installation hooks
ThemeManager::installFromZip($zipPath) fires:
theme.installing
- Fired: after successful extraction and strict manifest validation, before the install is committed.
- Arguments:
(string $slug, array $manifest) - Listener may throw: yes — a thrown exception aborts the install. The extracted directory is rolled back so you never end up with a half-installed theme.
- Typical uses: verify the installing user has permission, check a license server, scan the extracted files for unwanted contents.
theme.installed
- Fired: after the install is committed.
- Arguments:
(string $slug, array $manifest) - Listener may throw: technically yes, but the install is already on disk; throwing won't roll it back.
- Typical uses: run theme-shipped seeders, queue a thumbnail-generation job, notify the user, fire a webhook.
Update hooks
UpdateManager::updateTheme($slug) fires these. See themes/Updating for the
full update flow.
Added in 2.8.0.
theme.updating
- Fired: after an update has been found, before anything is backed up or downloaded.
- Arguments:
(string $slug, string $oldVersion, string $newVersion) - Listener may throw: yes — a thrown exception aborts the update. Nothing has changed on disk at that point, so a veto costs nothing and needs no rollback.
- Typical uses: pin a theme to a version, check a license server, refuse updates during a content freeze.
theme.updated
- Fired: after the new files are swapped in and caches are refreshed.
- Arguments:
(string $slug, string $newVersion, array $manifest) - Listener may throw: it propagates to the caller, but it will not roll the update back. The new files are already validated and in place; reverting them because a notification listener failed would be worse than surfacing the error.
- Typical uses: clear a CDN, rebuild assets, notify the user, fire a webhook.
Registering listeners
Register with addAction():
use function addAction;
// In a service provider's boot()
addAction('theme.activating', function (string $slug, $theme) {
if (! auth()->user()?->can('themes.activate')) {
throw new \RuntimeException('Not allowed to activate themes.');
}
});
addAction('theme.installed', function (string $slug, array $manifest) {
\Log::info('Theme installed', ['slug' => $slug, 'version' => $manifest['version'] ?? 'unknown']);
\App\Jobs\GenerateThemeThumbnail::dispatch($slug);
});
Listener priority
addAction() accepts an optional priority argument — lower numbers run first, default is 10. Use this to ensure your listener runs before or after another package's listener:
addAction('theme.installing', fn () => /* runs first */, 5);
addAction('theme.installing', fn () => /* runs default */);
addAction('theme.installing', fn () => /* runs last */, 20);
Order of operations
activateTheme($slug):
1. discoverThemes() (refresh cache)
2. resolveTheme($slug)
3. doAction('theme.activating', $slug, $theme) ← may throw to abort
4. update settings['themes.activeTheme'] = $slug
5. re-register view paths
6. doAction('theme.activated', $slug, $theme)
installFromZip($zipPath):
1. validateZip($zipPath)
2. extractZip($zipPath) → temporary directory
3. validateManifest($extractedManifest) ← may throw, rolls back
4. doAction('theme.installing', $slug, $manifest) ← may throw, rolls back
5. move extracted directory into themes path
6. doAction('theme.installed', $slug, $manifest)
updateTheme($slug):
1. checkThemeUpdate($slug) → no update? return early, no hooks fire
2. doAction('theme.updating', $slug, $old, $new) ← may throw to abort
3. download + verify checksum ← may throw, nothing touched yet
4. extract + validate into themes/.updates/ ← may throw, nothing touched yet
5. back up the installed directory
6. swap staged directory into place ← may throw, restores from backup
7. refresh caches + view paths
8. doAction('theme.updated', $slug, $new, $manifest) ← throwing does NOT roll back
Removing listeners
Use removeAction() to unregister a specific callback, or removeAllActions() to clear everything at a given priority (or for the whole hook):
use function removeAction;
use function removeAllActions;
$callback = fn (string $slug) => \Log::info('Activated', ['slug' => $slug]);
addAction('theme.activated', $callback);
// Remove just this callback
removeAction('theme.activated', $callback);
// Remove all callbacks at priority 5
removeAllActions('theme.activated', 5);
// Remove every listener for the hook
removeAllActions('theme.activated');

