Розробка кастомного плагіна 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: Мій плагін
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 днів