Grav Plugin Events — Complete Event Reference and Lifecycle
In this tutorial, you'll learn Grav plugin events — the complete event reference, request lifecycle from initialization to output, event-driven development patterns, and practical examples of the most important events.
What You'll Learn
- The complete Grav event lifecycle
- Request events: initialization, routing, page handling
- Content events: raw content, processed content, metadata
- Twig events: template paths, variables, extensions
- Output events: assets, final output
- Admin events: page management, plugin configuration
- Debugging and profiling events
Why It Matters
In WordPress, you use add_action() and add_filter() throughout your code. In Grav, events are dispatched at specific, predictable points. Knowing the event lifecycle lets you inject your code at exactly the right moment — modify content after it is loaded but before it is cached, add assets after plugins are initialized but before the page renders, or intercept form submissions before they are processed.
Real-World Use
A security plugin needs to scan all content for malicious code before it reaches the browser. It subscribes to onOutputGenerated, the last event before output is sent. The plugin scans the HTML for known attack patterns and blocks suspicious content. Because it runs at the latest possible event, it catches content modified by all other plugins too.
Learning Path
flowchart LR
A["Plugin Architecture"] --> B["Plugin Events
← You are here"]:::current
B --> C["Plugin Forms"]
C --> D["Plugin Admin"]
D --> E["Plugin CLI"]
E --> F["Multilingual"]
classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
Event Lifecycle
Grav events fire in a specific order during each request:
Phase 1: Initialization
| Event | When | What You Can Do |
|---|---|---|
onPluginsInitialized |
All plugins loaded | Initialize plugin, check conditions |
onAssetsInitialized |
Asset manager ready | Register CSS/JS assets |
onSessionInitialized |
Session started | Set session values, check auth |
onUserLogin |
User logged in | Log activity, update last login |
Phase 2: Page Handling
| Event | When | What You Can Do |
|---|---|---|
onPageNotFound |
Page not found (404) | Show custom 404, redirect |
onPageContentRaw |
Raw Markdown loaded | Modify Markdown before processing |
onPageProcessed |
Page YAML and content parsed | Read/modify processed page data |
onFolderMedia |
Media files in page folder | Filter or modify page media |
Phase 3: Twig Processing
| Event | When | What You Can Do |
|---|---|---|
onTwigTemplatePaths |
Template paths collected | Add custom template directories |
onTwigSiteVariables |
Twig variables prepared | Add custom Twig variables |
onTwigExtensions |
Twig extensions registered | Add custom filters/functions |
onTwigLoader |
Twig loader initialized | Custom template loading |
Phase 4: Output
| Event | When | What You Can Do |
|---|---|---|
onOutputGenerated |
Final HTML ready | Modify HTML, add tracking scripts |
onShutdown |
Request complete | Cleanup, log analytics |
Event Handler Patterns
Modifying Page Content
<?php
namespace Grav\Plugin;
use Grav\Common\Plugin;
use Grav\Common\Page\Page;
use RocketTheme\Toolbox\Event\Event;
class ContentPlugin extends Plugin
{
public static function getSubscribedEvents()
{
return [
'onPageContentRaw' => ['onPageContentRaw', 0],
'onPageProcessed' => ['onPageProcessed', 0],
];
}
// Modify raw Markdown before processing
public function onPageContentRaw(Event $event)
{
$page = $event['page'];
$content = $page->getRawContent();
// Replace custom markdown syntax
$content = str_replace('[current-year]', date('Y'), $content);
$page->setRawContent($content);
}
// Modify processed page data
public function onPageProcessed(Event $event)
{
$page = $event['page'];
$header = $page->header();
// Add reading time to frontmatter
$word_count = str_word_count(strip_tags($page->content()));
$header->reading_time = ceil($word_count / 200);
}
}
Adding Twig Variables
public static function getSubscribedEvents()
{
return [
'onTwigSiteVariables' => ['onTwigSiteVariables', 0],
];
}
public function onTwigSiteVariables()
{
// Add a global Twig variable
$this->grav['twig']->twig_vars['current_year'] = date('Y');
$this->grav['twig']->twig_vars['site_version'] = '2.3.1';
// Add data from external API
$stats = $this->getCachedStats();
$this->grav['twig']->twig_vars['site_stats'] = $stats;
}
In Twig:
<p>© {{ current_year }} DodaTech</p>
<p>Version {{ site_version }}</p>
Handling 404 Pages
public function onPageNotFound(Event $event)
{
$page = $event['page'];
// Log the 404
$this->grav['log']->warning('404: ' . $this->grav['uri']->route());
// Custom redirect for known old URLs
$redirects = [
'/old-page' => '/new-page',
'/deprecated' => '/current',
];
$route = $this->grav['uri']->route();
if (isset($redirects[$route])) {
$this->grav->redirect($redirects[$route], 301);
}
}
Modifying Output
public function onOutputGenerated(Event $event)
{
$output = $event->getOutput();
// Add Google Analytics tracking code before </body>
$tracking = <<<HTML
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_ID');
</script>
HTML;
$output = str_replace('</body>', $tracking . '</body>', $output);
// Add a performance header
$event->setOutput($output);
$this->grav->header->set('X-Grav-Time', microtime(true) - GRAV_REQUEST_TIME);
}
Handling Assets
public function onAssetsInitialized()
{
$assets = $this->grav['assets'];
// Add conditional CSS based on page type
$page = $this->grav['page'];
if ($page && $page->template() == 'blog') {
$assets->addCss('theme://css/blog.css');
}
// Add JS with defer
$assets->addJs('theme://js/lazy.js', [
'loading' => 'defer',
'group' => 'footer'
]);
}
Admin Events
| Event | When | What You Can Do |
|---|---|---|
onAdminPage |
Admin page rendered | Modify admin page content |
onAdminSave |
Page saved in admin | Validate, modify before save |
onAdminAfterSave |
Page saved successfully | Post-save actions (cache clear) |
onAdminDelete |
Page about to be deleted | Prevent deletion, cleanup |
onDashboard |
Admin dashboard rendered | Add dashboard widgets |
onAdminMenu |
Admin menu built | Add menu items |
Plugin Admin Hook Example
public static function getSubscribedEvents()
{
return [
'onAdminSave' => ['onAdminSave', 0],
'onAdminAfterSave' => ['onAdminAfterSave', 0],
];
}
public function onAdminSave(Event $event)
{
$page = $event['page'];
$header = $page->header();
// Validate custom field
if (isset($header->featured) && !is_bool($header->featured)) {
throw new \RuntimeException('Featured must be true or false');
}
}
public function onAdminAfterSave(Event $event)
{
// Regenerate sitemap after page save
$sitemap = $this->grav['sitemap'] ?? null;
if ($sitemap) {
$sitemap->generate();
}
}
Plugin Events in Admin Context
Check if running in admin:
public function onPluginsInitialized()
{
if ($this->isAdmin()) {
$this->enable([
'onAdminSave' => ['onAdminSave', 0],
'onAdminDashboard' => ['onAdminDashboard', 0],
]);
return;
}
// Frontend events
$this->enable([
'onPageContentRaw' => ['onPageContentRaw', 0],
'onOutputGenerated' => ['onOutputGenerated', 0],
]);
}
Learning Path
flowchart LR
A["Plugin Architecture"] --> B["Plugin Events
← You are here"]:::current
B --> C["Plugin Forms"]
C --> D["Plugin Admin"]
D --> E["Plugin CLI"]
classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
Common Mistakes
Modifying content in the wrong event: If you modify Markdown after
onPageProcessed, your changes may be overwritten. UseonPageContentRawfor Markdown modifications andonPageProcessedfor post-processing.Not using event priority correctly: If your plugin depends on another plugin's output, use a higher priority number to run later. If your plugin provides data for others, use a lower number to run earlier.
Heavy processing in
onPluginsInitialized: This event runs on every request. Keep initialization light. Move heavy processing to later events or cache results.Not checking
isAdmin()for admin-only events: Admin events likeonAdminSaveshould be registered inside anisAdmin()check to prevent errors on frontend requests.Forgetting to return the event object: Some events expect you to return or modify the event object. Check the event's behavior before writing the handler.
Practice Questions
What is the last event before Grav sends output to the browser? Answer:
onOutputGenerated. This event receives the final HTML output and can modify it before it is sent to the client.How do you add a custom variable to all Twig templates? Answer: Subscribe to
onTwigSiteVariablesand set$this->grav['twig']->twig_vars['my_var'] = $value. The variable is then available in all templates.What event would you use to modify page content before it is cached? Answer:
onPageContentRaw, which fires before the content is processed and cached. Changes made here are reflected in the cached version.How do you register admin-specific events conditionally? Answer: Check
$this->isAdmin()insideonPluginsInitialized()and call$this->enable([...])with admin-specific events only when in the admin context.Challenge: Build a plugin that monitors page saves and sends notifications. Subscribe to
onAdminAfterSave, detect if the page was published, construct a notification message with the page title and URL, and send it through email or a Webhook. Include logging of all notifications sent, proper error handling if the webhook fails, and a setting to enable/disable notifications per page type.
FAQ
Mini Project
Goal: Build an event-driven plugin that hooks into 6 different lifecycle points.
- Create a plugin that subscribes to
onPageContentRaw,onPageProcessed,onTwigSiteVariables,onOutputGenerated,onAdminSave, andonShutdown - In
onPageContentRaw: add a[last-modified]placeholder replacement - In
onPageProcessed: calculate and store reading time - In
onTwigSiteVariables: addcurrent_yearandsite_build_time - In
onOutputGenerated: add a performance comment at the end of HTML - In
onAdminSave: validate that the title is not empty - In
onShutdown: log the request time and URL - Add a debug mode that outputs all events with their execution times
- Test the plugin with different page types and admin operations
- Document event execution order with timing output
What's Next
Now you understand all Grav events. Next, learn plugin form handling:
Continue to Lesson 26: Plugin Forms — Form state, validation, file uploads, and multi-step forms.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro