Перейти к содержанию

Урок по созданию плагина

Плагины обычно создают, когда задачу нельзя решить средствами ядра Grav.

В этом уроке мы создадим плагин, который помогает Grav отдавать пользователю случайную страницу. Вы наверняка видели похожую функциональность на сайтах блогов: кнопку, по нажатию на которую открывается случайная запись.

Предупреждение

Поскольку для этой задачи уже есть плагин Random, мы назовём наш тестовый плагин Randomizer.

Эту фичу нельзя получить из коробки, но её легко реализовать с помощью плагина. Как и во многих аспектах Grav, здесь нет единственного способа. Наоборот, есть много вариантов. Мы рассмотрим только один подход…

Обзор плагина Randomizer

В нашем плагине мы выберем следующий подход:

  1. Активировать плагин, если URI совпадает с настроенным «маршрутом-триггером» (например, /random).

  2. Создать фильтр, чтобы в пул случайных страниц попадали только настроенные таксономии (например, category: blog).

  3. Найти случайную страницу из отфильтрованного пула и указать Grav использовать её для контента страницы.

Хорошо! Звучит достаточно просто, правда? Тогда приступим!

Шаг 1 — Установка плагина DevTools

Внимание

В предыдущих версиях урока требовалось создавать плагин вручную. Весь этот процесс можно пропустить благодаря новому плагину DevTools.

Первый шаг в создании нового плагина — установить плагин DevTools. Это можно сделать двумя способами.

Установка через CLI GPM

  • Перейдите в командной строке в корень установки Grav.
bin/gpm install devtools

Установка через админку

  • После входа перейдите в раздел Плагины в боковой панели.
  • Нажмите кнопку Добавить в правом верхнем углу.
  • Найдите DevTools в списке и нажмите Установить.

Шаг 2 — Создание плагина Randomizer

Для следующего шага вам действительно понадобится командная строка, потому что DevTools предоставляет несколько CLI-команд, которые значительно упрощают создание нового плагина!

Из корня установки Grav выполните команду:

bin/plugin devtools new-plugin

В процессе вас спросят несколько вопросов, необходимых для создания нового плагина:

bin/plugin devtools new-plugin
Enter Plugin Name: Randomizer
Enter Plugin Description: Sends the user to a random page
Enter Developer Name: Acme Corp
Enter Developer Email: contact@acme.co

SUCCESS plugin Randomizer -> Created Successfully

Path: /www/user/plugins/randomizer

Make sure to run I4 to initialize the autoloader

Предупреждение

На этом этапе нужно выполнить composer update в только что созданной папке плагина.

Команда DevTools сообщает, где создан новый плагин. Созданный плагин полностью рабочий, но в нём не будет логики, выполняющей то, что нам нужно. Нам придётся изменить его под свои нужды.

Шаг 3 — Основы плагина

Теперь у нас есть новый плагин, который можно изменять и развивать. Давайте разберём, из чего состоит плагин. Если заглянуть в папку user/plugins/randomizer, вы увидите:

.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── blueprints.yaml
├── randomizer.php
└── randomizer.yaml

Это примерная структура, но некоторые элементы обязательны:

Обязательные элементы для работы

Эти элементы критичны, и плагин не будет надёжно работать без них.

  • blueprints.yaml — файл конфигурации, из которого Grav получает информацию о плагине. В нём также можно определить форму, которую админка показывает при просмотре сведений о плагине. Эта форма позволяет сохранять настройки плагина. Этот файл описан в главе о формах.
  • randomizer.php — имя этого файла соответствует вашему плагину, и в нём может находиться любая логика плагина. Можно использовать любой хук события плагина, чтобы выполнить логику практически на любом этапе жизненного цикла Grav.
  • randomizer.yaml — конфигурация, используемая плагином для задания параметров. Имя должно совпадать с именем файла .php.

Обязательные элементы для выпуска

Эти элементы нужны, если вы хотите выпустить плагин через GPM.

  • CHANGELOG.md — файл, следующий формату списка изменений Grav, где показаны изменения в релизах.
  • LICENSE — файл лицензии; вероятно, MIT, если у вас нет особой необходимости в другой.
  • README.md — «Readme», который должен содержать документацию по плагину: как установить, настроить и использовать.

Шаг 4 — Конфигурация плагина

Как описано в обзоре плагина, нам нужны несколько параметров конфигурации, поэтому файл randomizer.yaml должен выглядеть примерно так:

1
2
3
4
5
enabled: true
active: true
route: /random
filters:
    category: blog

Это позволяет задать несколько фильтров, если нужно, но пока мы хотим, чтобы в случайный выбор попадал только контент с таксономией category: blog.

У всех плагинов должна быть опция enabled. Если в конфигурации сайта она равна false, ваш плагин никогда не будет инициализирован Grav. У всех плагинов также есть опция active. Если в конфигурации сайта она равна false, каждой странице нужно будет активировать плагин. Обратите внимание: многие плагины также поддерживают enabled/active в метаданных страницы через mergeConfig, как описано ниже.

Совет

В установке Grav по умолчанию таксономии category и tag уже определены. Эту конфигурацию можно изменить в файле user/config/site.yaml.

Конечно, как и с любой другой конфигурацией Grav, для повседневного управления не рекомендуется трогать конфигурацию по умолчанию. Вместо этого создайте переопределение в файле /user/config/plugins/randomizer.yaml для пользовательских настроек. Файл randomizer.yaml, поставляемый с плагином, действительно предназначен для разумных значений по умолчанию.

Шаг 5 — Базовая структура плагина

Базовая структура класса плагина уже будет выглядеть примерно так:

<?php
namespace Grav\Plugin;

use Composer\Autoload\ClassLoader;
use Grav\Common\Plugin;
use RocketTheme\Toolbox\Event\Event;

/**
 * Class RandomizerPlugin
 * @package Grav\Plugin
 */
class RandomizerPlugin extends Plugin
{
    /**
     * Composer autoload.
     *
     * @return ClassLoader
     */
    public function autoload(): ClassLoader
    {
        return require __DIR__ . '/vendor/autoload.php';
    }
}

Нам нужно добавить несколько инструкций use, потому что мы будем использовать эти классы в плагине. Это экономит место и делает код читаемее, если не указывать полное пространство имён каждого класса внутри кода.

Измените инструкции use так:

1
2
3
4
5
use Composer\Autoload\ClassLoader;
use Grav\Common\Plugin;
use Grav\Common\Page\Collection;
use Grav\Common\Uri;
use Grav\Common\Taxonomy;

Две ключевые части этой структуры класса:

  1. В верхней части PHP-файла плагина должно быть namespace Grav\Plugin.
  2. Плагины следует называть в TitleCase на основе имени плагина с добавлением строки Plugin в конце, и они должны расширять Plugin; отсюда имя класса RandomizerPlugin.

Шаг 6 — Подписка на события

Grav использует сложную систему событий, и для оптимальной производительности Grav проверяет все плагины, чтобы определить, на какие события они подписаны.

1
2
3
4
5
6
7
8
9
public static function getSubscribedEvents(): array
{
    return [
        'onPluginsInitialized' => [
            ['autoload', 100000], // TODO: удалить, когда плагину потребуется Grav >=1.7
            ['onPluginsInitialized', 0]
        ]
    ];
}

В этом плагине мы сообщим Grav, что подписываемся на событие onPluginsInitialized. Так мы сможем использовать это событие (первое доступное плагинам), чтобы решить, подписываться ли на другие события.

Примечание

Примечание: первый слушатель события autoload нужен только в Grav 1.6. Grav 1.7 вызывает этот метод автоматически.

Шаг 7 — Определение, должен ли плагин работать

Следующий шаг — добавить в класс RandomizerPlugin метод обработки события onPluginsInitialized, чтобы плагин активировался, только когда пользователь пытается перейти по маршруту, настроенному в файле randomizer.yaml. Замените текущую «примерную» логику плагина на следующую:

public function onPluginsInitialized(): void
{
    // Не продолжать, если мы в админке
    if ($this->isAdmin()) {
        return;
    }

    /** @var Uri $uri */
    $uri = $this->grav['uri'];
    $config = $this->config();

    $route = $config['route'] ?? null;
    if ($route && $route == $uri->path()) {
        $this->enable([
            'onPageInitialized' => ['onPageInitialized', 0]
        ]);
    }
}

Сначала мы получаем объект Uri из контейнера внедрения зависимостей. Он содержит всю информацию о текущем URI, включая информацию о маршруте.

Метод config() уже есть в базовом классе Plugin, поэтому его можно просто использовать, чтобы получить значение конфигурации для настроенного route.

Затем мы сравниваем настроенный маршрут с текущим путём URI. Если они равны, мы сообщаем диспетчеру, что плагин также будет слушать новое событие: onPageInitialized.

Такой подход гарантирует, что мы не выполним лишний код, если он не нужен. Подобные практики помогают сайту работать максимально быстро.

Шаг 8 — Показ случайной страницы

Последний шаг нашего плагина — показать случайную страницу. Это делается добавлением следующего метода:

/**
 * Отправляет пользователя на случайную страницу
 */
public function onPageInitialized(): void
{
    /** @var Taxonomy $uri */
    $taxonomy_map = $this->grav['taxonomy'];
    $config = $this->config();

    $filters = (array)($config['filters'] ?? []);
    $operator = $config['filter_combinator'] ?? 'and';

    if (count($filters) > 0) {
        $collection = new Collection();
        $collection->append($taxonomy_map->findTaxonomy($filters, $operator)->toArray());
        if (count($collection) > 0) {
            unset($this->grav['page']);
            $this->grav['page'] = $collection->random()->current();
        }
    }
}

Этот метод немного сложнее, поэтому разберём, что происходит:

  1. Сначала мы получаем объект Taxonomy из DI-контейнера Grav и присваиваем его переменной $taxonomy_map.

  2. Затем получаем массив фильтров из конфигурации плагина. В нашей конфигурации это массив с 1 элементом: ['category' => 'blog'].

  3. Проверяем, что фильтры есть, затем создаём новую Collection в переменной $collection для хранения страниц.

  4. Добавляем в $collection все страницы, соответствующие фильтру.

  5. Удаляем текущий объект page, известный Grav.

  6. Устанавливаем текущую page в случайный элемент коллекции.

Шаг 9 — Очистка

Пример плагина, созданный плагином DevTools, использовал событие onPageContentRaw(). В нашем новом плагине это событие не используется, поэтому функцию можно смело удалить.

Шаг 10 — Итоговый класс плагина

Вот и всё! Плагин готов. Итоговый класс плагина должен выглядеть примерно так:

<?php
namespace Grav\Plugin;

use Composer\Autoload\ClassLoader;
use Grav\Common\Plugin;
use Grav\Common\Page\Collection;
use Grav\Common\Uri;
use Grav\Common\Taxonomy;

/**
 * Class RandomizerPlugin
 * @package Grav\Plugin
 */
class RandomizerPlugin extends Plugin
{
    /**
     * @return array
     *
     * Метод getSubscribedEvents() даёт ядру список событий,
     *     на которые хочет подписаться плагин. Ключ каждой
     *     секции массива — событие, которое слушает плагин,
     *     а значение (в виде массива) содержит вызываемый
     *     объект (или функцию) и приоритет. Чем выше число,
     *     тем выше приоритет.
     */
    public static function getSubscribedEvents(): array
    {
    return [
        'onPluginsInitialized' => [
            ['autoload', 100000], // TODO: удалить, когда плагину потребуется Grav >=1.7
            ['onPluginsInitialized', 0]
        ]
    ];
    }

    /**
     * Composer autoload.
     *
     * @return ClassLoader
     */
    public function autoload(): ClassLoader
    {
        return require __DIR__ . '/vendor/autoload.php';
    }

    public function onPluginsInitialized(): void
    {
        // Не продолжать, если мы в админке
        if ($this->isAdmin()) {
            return;
        }

        /** @var Uri $uri */
        $uri = $this->grav['uri'];
        $config = $this->config();

        $route = $config['route'] ?? null;
        if ($route && $route == $uri->path()) {
            $this->enable([
                'onPageInitialized' => ['onPageInitialized', 0]
            ]);
        }
    }

    /**
     * Отправляет пользователя на случайную страницу
     */
    public function onPageInitialized(): void
    {
        /** @var Taxonomy $uri */
        $taxonomy_map = $this->grav['taxonomy'];
        $config = $this->config();

        $filters = (array)($config['filters'] ?? []);
        $operator = $config['filter_combinator'] ?? 'and';

        if (count($filters) > 0) {
            $collection = new Collection();
            $collection->append($taxonomy_map->findTaxonomy($filters, $operator)->toArray());
            if (count($collection) > 0) {
                unset($this->grav['page']);
                $this->grav['page'] = $collection->random()->current();
            }
        }
    }
}

Если вы всё повторяли, у вас должен быть полностью рабочий плагин Randomizer, включённый на сайте. Просто откройте в браузере http://my-site.example/random, и вы увидите случайную страницу. Также можно скачать оригинальный плагин Random в разделе Plugins Download на сайте getgrav.org.

Расширение схем

Если плагину нужно расширять схемы, например default.yaml из /system/blueprints/pages/default.yaml, регистрировать схему через хуки не нужно, если соблюдать структуру папок, разместив расширяемую схему в [your-plugin-directory]/blueprints/pages/default.yaml. Grav объединит ваши расширенные определения схемы, а темы смогут сделать то же самое позже.

наследование system, если создать такую структуру папок внутри плагина

  • blueprints
    • pages
      • default.yaml

наследование admin, если создать такую структуру папок внутри плагина

  • blueprints
    • admin
      • pages
        • raw.yaml

В админке нужно наследовать raw или другие, и схемы тем, расширяющие default, не попадут в вашу конфигурацию.

Если это не страницы, делайте то же самое для других наследований… Так вы сможете свести изменения при расширении к минимуму — в этом и смысл расширения :-).

Объединение конфигурации плагина и страницы

Одна популярная техника, используемая в разных плагинах, — объединение конфигурации плагина (по умолчанию или переопределённой пользователем) с конфигурацией уровня страницы. Это значит, что можно задать конфигурацию для всего сайта, а затем при необходимости задать отдельную конфигурацию для конкретной страницы. Это даёт плагину много мощности и гибкости.

В последних версиях Grav добавлен вспомогательный метод, выполняющий эту функциональность автоматически, чтобы вам не приходилось писать эту логику самим. Плагин SmartyPants — хороший пример такой функциональности в действии:

public function onPageContentProcessed(Event $event): void
{
    $page = $event['page'];
    $config = $this->mergeConfig($page);

    if ($config->get('process_content')) {
        $page->setRawContent(\Michelf\SmartyPants::defaultTransform(
            $page->getRawContent(),
            $config->get('options')
        ));
    }
}

Реализация CLI в плагине

Плагины также могут интегрироваться с командной строкой bin/plugin для выполнения задач. Если хотите реализовать такую функциональность, следуйте документации по CLI для плагинов.