Grav Plugin Architecture — Structure, Events and DI Container
In this tutorial, you'll learn Grav plugin architecture — the plugin folder structure, event system, Dependency Injection container, service providers, and how to build a well-structured plugin from scratch.
What You'll Learn
- Grav plugin folder structure and required files
- The event system: how events, subscribers, and hooks work
- The DI container: accessing Grav services
- Plugin configuration and blueprints
- Building a complete plugin from scratch
- Best practices for plugin development
Why It Matters
In WordPress, plugins use hooks (actions and filters) and a global functions API. In Grav, plugins use an event system similar to Symfony. Events are dispatched at specific points in the request lifecycle, and plugins subscribe to the events they care about. This architecture is cleaner, more testable, and more predictable than global hooks. Understanding the plugin architecture lets you extend Grav in any way imaginable — from adding a simple shortcode to building a full-featured e-commerce system.
Real-World Use
A team needs to generate a sitemap automatically when pages are saved. They build a plugin that subscribes to the onPageSaved event. Every time a page is saved, the plugin regenerates the XML sitemap. No cron job needed, no manual regeneration. The plugin is a single PHP class with one event subscriber method.
Learning Path
flowchart LR
A["Theme Languages"] --> B["Plugin Architecture
← You are here"]:::current
B --> C["Plugin Events"]
C --> D["Plugin Forms"]
D --> E["Plugin Admin"]
E --> F["Plugin CLI"]
F --> G["Multilingual"]
classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
Plugin Folder Structure
A Grav plugin follows a standard structure:
user/plugins/myplugin/
├── myplugin.php # Plugin bootstrap
├── myplugin.yaml # Default configuration
├── blueprints.yaml # Admin configuration form
├── languages/ # Translation files
│ └── en.yaml
├── classes/ # PHP classes
│ ├── MyPlugin.php
│ └── TwigExtension.php
├── templates/ # Plugin templates
│ └── partials/
├── assets/ # Plugin assets
│ ├── css/
│ └── js/
├── vendor/ # Composer dependencies
└── README.md
Required Files
Every plugin needs two files at minimum:
- Plugin Bootstrap (
myplugin.php) — registers events - Default config (
myplugin.yaml) — default configuration values
The Plugin Bootstrap File
user/plugins/myplugin/myplugin.php:
<?php
namespace Grav\Plugin;
use Grav\Common\Plugin;
use Grav\Common\Page\Page;
class MyPlugin extends Plugin
{
// Event subscription method
public static function getSubscribedEvents()
{
return [
'onPluginsInitialized' => ['onPluginsInitialized', 0],
'onPageContentRaw' => ['onPageContentRaw', 0],
'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0],
];
}
// Called when all plugins are initialized
public function onPluginsInitialized()
{
// Only run on non-admin pages
if ($this->isAdmin()) {
return;
}
echo 'MyPlugin initialized!';
}
// Called before page content is processed
public function onPageContentRaw(Event $event)
{
$page = $event['page'];
$content = $page->getRawContent();
// Modify content
$page->setRawContent($content);
}
// Add plugin template paths
public function onTwigTemplatePaths()
{
$this->grav['twig']->twig_paths[] = __DIR__ . '/templates';
}
}
The Event System
Grav dispatches events at specific points during the request lifecycle. Plugins subscribe to events to take action.
Subscribing to Events
The getSubscribedEvents() method returns an array of event names with handler methods:
public static function getSubscribedEvents()
{
return [
'onPageContentRaw' => ['onPageContentRaw', 0],
// event name handler priority
];
}
Priority determines execution order. Lower priority values run earlier. Default is 0. Range is typically 0-10.
Event Lifecycle
Major events in order:
onPluginsInitialized— All plugins loadedonPageContentRaw— Raw Markdown available for modificationonPageProcessed— Page processed (YAML parsed, content ready)onTwigTemplatePaths— Template paths being collectedonTwigSiteVariables— Twig variables being preparedonTwigExtensions— Twig extensions being registeredonOutputGenerated— Final HTML output ready to send
The Event Object
Events pass an array-like object:
public function onPageContentRaw(Event $event)
{
$page = $event['page'];
$content = $event['content'];
// Or access through Grav
$grav = $this->grav;
$uri = $this->grav['uri'];
$config = $this->grav['config'];
}
The DI Container
Grav uses a dependency injection container (Pimple-based) to manage services:
// Access services through the container
$this->grav['cache'] // Cache service
$this->grav['config'] // Configuration service
$this->grav['uri'] // URI handling
$this->grav['pages'] // Page management
$this->grav['twig'] // Twig templating
$this->grav['assets'] // Asset manager
$this->grav['language'] // Language handling
$this->grav['plugins'] // Plugin management
$this->grav['session'] // Session handling
$this->grav['accounts'] // User accounts
$this->grav['inflector'] // String manipulation
Adding Custom Services
Plugins can register custom services:
public function onPluginsInitialized()
{
$this->grav['myplugin.service'] = function($c) {
return new MyService($c['config'], $c['cache']);
};
}
Plugin Configuration
Default Config
user/plugins/myplugin/myplugin.yaml:
enabled: true
api_key: ''
cache_duration: 3600
items_per_page: 10
features:
enable_search: true
enable_sharing: false
Blueprints for Admin
user/plugins/myplugin/blueprints.yaml:
name: My Plugin
version: 1.0.0
description: A custom Grav plugin
icon: plug
author:
name: DodaTech
email: support@dodatech.com
keywords: grav, plugin, custom
form:
validation: loose
fields:
enabled:
type: toggle
label: Plugin enabled
default: 1
api_key:
type: text
label: API Key
size: large
cache_duration:
type: range
label: Cache duration (seconds)
default: 3600
validate:
min: 60
max: 86400
features.enable_search:
type: toggle
label: Enable search
default: 1
Accessing Configuration
public function onPageContentRaw()
{
$enabled = $this->grav['config']->get('plugins.myplugin.enabled');
$api_key = $this->grav['config']->get('plugins.myplugin.api_key');
$features = $this->grav['config']->get('plugins.myplugin.features');
}
In Twig:
{{ config.plugins.myplugin.api_key }}
The Plugin Class Hierarchy
Grav\Common\Plugin # Base plugin class
└── MyPlugin # Your plugin
└── methods:
getSubscribedEvents() # Register event subscribers
onPluginsInitialized() # Initialize plugin
The base Plugin class provides:
$this->grav— access to DI container$this->config()— plugin configuration$this->isAdmin()— check if in admin context$this->enable()— enable specific events$this->mergeConfig()— merge config with defaults
Learning Path
flowchart LR
A["Theme Languages"] --> B["Plugin Architecture
← You are here"]:::current
B --> C["Plugin Events"]
C --> D["Plugin Forms"]
D --> E["Plugin Admin"]
classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
Common Mistakes
Not checking
isAdmin()for frontend-only code: If your plugin should only run on frontend pages, checkif ($this->isAdmin()) { return; }first. Without this, the plugin runs in the Admin panel too.Overriding
getSubscribedEvents()incorrectly: The method must bepublic staticand return an array. Each key is an event name; each value is an array with handler method and priority.Not using namespaces: All plugin classes should use namespaces to avoid conflicts. The convention is
Grav\Plugin\YourPlugin.Hardcoding configuration: Use
$this->grav['config']->get()instead of hardcoding values. Configuration in YAML files can be changed through the Admin panel.Ignoring cache: If your plugin reads data from files or APIs, cache the results. Use
$this->grav['cache']->fetch()and->save()to avoid repeating expensive operations.
Practice Questions
What method must a plugin implement to subscribe to events? Answer:
getSubscribedEvents()— a static method that returns an array mapping event names to handler methods with priority values.How do you access Grav services from within a plugin? Answer: Through the DI container:
$this->grav['cache'],$this->grav['config'],$this->grav['pages'], etc. Each service is accessible as a key on the container.What is the purpose of
blueprints.yamlin a plugin? Answer: It defines form fields for the Admin panel, allowing site administrators to configure the plugin through the UI. It also provides metadata about the plugin (name, version, author).How do you prevent a plugin from executing in the Admin panel? Answer: Call
if ($this->isAdmin()) { return; }at the beginning ofonPluginsInitialized(). This ensures plugin code only runs on the frontend.Challenge: Build a complete plugin from scratch that tracks page views. Include proper folder structure, event subscription, configuration in YAML, admin blueprints, Caching for performance, a service class for business logic, Twig extension for template access, and language files for translations. The plugin should store page view counts in the cache and display them with a Twig function.
FAQ
Mini Project
Goal: Build a complete Grav plugin with proper architecture.
- Create plugin directory with bootstrap file, config, and blueprints
- Subscribe to 3 different events
- Register a custom Twig extension
- Add multilingual support with language files
- Implement caching for expensive operations
- Add plugin configuration with admin blueprints
- Create a service class for business logic
- Add template overrides through Twig template paths
- Add proper error handling with try-catch blocks
- Test the plugin and verify event execution order
What's Next
Now you understand plugin architecture. Next, explore all Grav events in detail:
Continue to Lesson 25: Plugin Events — Complete event reference, onPageContentRaw, onTwigSiteVariables, and all lifecycle events.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro