Розробка користувацького плагіна Grav
Плагін Grav — це PHP-клас, який підписується на події життєвого циклу запиту. Grav публікує понад 30 подій: від ініціалізації до рендерингу сторінки та відправки відповіді. Плагін перехоплює потрібні події та модифікує поведінку системи без змін ядра.
Структура плагіна
user/plugins/my-plugin/
my-plugin.php # основний клас плагіна
my-plugin.yaml # конфігурація за замовчуванням
blueprints.yaml # метадані (для GPM та адмін-панелі)
languages.yaml # переводи інтерфейсу
README.md
CHANGELOG.md
classes/ # допоміжні класи
MyService.php
templates/ # Twig-шаблони, якщо плагін їх додає
my-plugin.html.twig
assets/
css/
my-plugin.css
js/
my-plugin.js
blueprints.yaml
name: Мій плагін
version: 1.2.0
description: Опис функціональності плагіна
icon: plug
author:
name: Ім'я розробника
email: [email protected]
homepage: https://example.com
bugs: https://github.com/user/grav-plugin-my-plugin/issues
license: MIT
dependencies:
- { name: grav, version: '>=1.7.0' }
- { name: form, version: '>=7.0.0' }
form:
validation: strict
fields:
enabled:
type: toggle
label: Статус плагіна
highlight: 1
default: 0
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
api_key:
type: text
label: API ключ
size: large
cache_ttl:
type: number
label: Cache TTL (секунди)
default: 3600
validate:
type: int
min: 60
max: 86400
Основний клас плагіна
<?php
// my-plugin.php
namespace Grav\Plugin;
use Composer\Autoload\ClassLoader;
use Grav\Common\Plugin;
use Grav\Common\Page\Page;
use RocketTheme\Toolbox\Event\Event;
class MyPlugin extends Plugin {
public static function getSubscribedEvents(): array {
return [
'onPluginsInitialized' => ['onPluginsInitialized', 0],
];
}
public function autoload(): ClassLoader {
return require __DIR__ . '/vendor/autoload.php';
}
public function onPluginsInitialized(): void {
if ($this->isAdmin()) {
return; // не виконувати в адмін-панелі
}
if (!$this->config->get('plugins.my-plugin.enabled')) {
return;
}
$this->enable([
'onPageInitialized' => ['onPageInitialized', 0],
'onPageContentRaw' => ['onPageContentRaw', 0],
'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0],
'onTwigSiteVariables' => ['onTwigSiteVariables', 0],
'onOutputGenerated' => ['onOutputGenerated', -10],
]);
}
public function onPageInitialized(Event $event): void {
/** @var Page $page */
$page = $event['page'];
// Пропустити сторінки без потрібного заголовка
if (!isset($page->header()->my_plugin)) {
return;
}
// Додати CSS/JS на сторінку
$this->grav['assets']->addCss('plugin://my-plugin/assets/css/my-plugin.css');
$this->grav['assets']->addJs('plugin://my-plugin/assets/js/my-plugin.js', ['loading' => 'defer']);
}
public function onPageContentRaw(Event $event): void {
/** @var Page $page */
$page = $event['page'];
$raw = $page->getRawContent();
// Замінити shortcode [my-tag attr="val"]...[/my-tag]
$processed = preg_replace_callback(
'/\[my-tag([^\]]*)\](.*?)\[\/my-tag\]/s',
function(array $matches): string {
$attrs = $this->parseAttrs($matches[1]);
$content = $matches[2];
return $this->renderTag($attrs, $content);
},
$raw
);
$page->setRawContent($processed);
}
public function onTwigTemplatePaths(): void {
$this->grav['twig']->twig_paths[] = __DIR__ . '/templates';
}
public function onTwigSiteVariables(): void {
$this->grav['twig']->twig_vars['my_plugin_data'] = $this->getPluginData();
}
public function onOutputGenerated(): void {
$output = $this->grav->output;
// Внедрити аналітичний код перед </body>
$snippet = '<script>/* analytics */</script>';
$this->grav->output = str_replace('</body>', $snippet . '</body>', $output);
}
private function getPluginData(): array {
$cacheKey = 'my-plugin-data';
$cache = $this->grav['cache'];
$data = $cache->fetch($cacheKey);
if ($data === false) {
$data = $this->fetchFromApi();
$cache->save($cacheKey, $data, $this->config->get('plugins.my-plugin.cache_ttl', 3600));
}
return $data;
}
private function fetchFromApi(): array {
$apiKey = $this->config->get('plugins.my-plugin.api_key');
$ch = curl_init("https://api.example.com/v1/data");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiKey"],
CURLOPT_TIMEOUT => 5,
]);
$result = curl_exec($ch);
curl_close($ch);
return json_decode($result, true) ?? [];
}
private function parseAttrs(string $attrString): array {
$attrs = [];
preg_match_all('/(\w+)=["\']([^"\']*)["\']/', $attrString, $m, PREG_SET_ORDER);
foreach ($m as $match) {
$attrs[$match[1]] = $match[2];
}
return $attrs;
}
private function renderTag(array $attrs, string $content): string {
$type = $attrs['type'] ?? 'info';
return "<div class=\"my-tag my-tag--$type\">$content</div>";
}
}
Плагін з REST API-ендпоїнтами
Grav дозволяє реєструвати маршрути через подію onTask:
// У getSubscribedEvents():
'onTask.myPlugin.submit' => ['onTaskSubmit', 0],
// або через маршрути Grav 1.7+:
'onPagesInitialized' => ['registerRoutes', 0],
public function registerRoutes(): void {
$this->grav['router']->addRoute('/api/my-plugin/data', ['GET'], function() {
header('Content-Type: application/json');
echo json_encode($this->getPluginData());
exit;
});
}
my-plugin.yaml — Конфігурація
enabled: false
api_key: ''
cache_ttl: 3600
debug: false
allowed_pages:
- /services
- /blog
Читання в плагіні: $this->config->get('plugins.my-plugin.api_key').
Перевизначення конфігурації для сторінок
Конфіг плагіна можна перевизначити у frontmatter сторінки:
---
title: Особлива сторінка
my-plugin:
cache_ttl: 60
debug: true
---
// У плагіні — отримати конфіг з урахуванням page-override
$config = $this->mergeConfig($this->grav['page']);
$ttl = $config->get('cache_ttl', 3600);
Тестування плагіна
# Увімкнути режим отладки
# user/config/system.yaml: debugger.enabled: true
# Побачити доступні події
bin/grav plugin my-plugin list-events
# Очистити кеш після змін
bin/grav cache:clear
Терміни розробки
| Тип плагіна | Термін |
|---|---|
| Shortcode / обробка контенту | 4–12 год |
| Інтеграція з зовнішнім API + кеш | 1–3 дні |
| Користувацька форма з обробкою | 1–2 дні |
| REST API-ендпоїнти (3–5 маршрутів) | 1–2 дні |
| Повний функціональний плагін з UI | 3–7 днів |







