Разработка кастомного плагина Grav

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка кастомного плагина Grav
Средняя
~2-3 рабочих дня
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Разработка кастомного плагина 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: My Plugin
version: 1.2.0
description: Описание функциональности плагина
icon: plug
author:
  name: Dev 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: Plugin Status
      highlight: 1
      default: 0
      options:
        1: PLUGIN_ADMIN.ENABLED
        0: PLUGIN_ADMIN.DISABLED
      validate:
        type: bool

    api_key:
      type: text
      label: API Key
      size: large

    cache_ttl:
      type: number
      label: Cache TTL (seconds)
      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 дней