Анатомия плагина OctoberCMS

Функционал OctoberCMS, как и любой другой CMS, можно расширять. Единицей такого расширения является плагин. В этой заметке я расскажу немного о том, что такое плагин для OctoberCMS, как его сделать, из чего он состоит и что куда класть внутри.

Большую часть информации, рассматриваемой далее, можно почерпнуть из официальной документации OctoberCMS. В этой заметке я постараюсь не столько пересказать содержимое доков, сколько обобщить то, что мне удалось выяснить в процессе разработки и использования плагинов для October.

Где взять плагины

Главным местом обитания плагинов для OctoberCMS является маркет, плагины там бывают как бесплатные, так и платные. Основное удобство маркета — быстрая инсталляция. Кроме того, на маркете можно объединять плагины в проекты (сборки), но об этом как-нибудь в другой раз.

Плагин с маркета устанавливается в один клик из админки (в которой есть удобный поиск):

Установка плагинов

Кстати, не каждый сразу найдет, где именно находится установка плагинов. Она находится в меню «Настройки», пункт Система → Обновления (/backend/system/updates). Так уж вышло.

А еще можно установить плагин в одну строчку командой artisan:

php artisan plugin:install RainLab.Blog

и удаляется так же быстро:

php artisan plugin:remove RainLab.Blog

Здесь RainLab — имя автора плагина, а Blog — название.

Плагины, которым повезло меньше и они (пока) отсутствуют на маркете, обитают обычно, как и сам October, на гитхабе. Там же обитают и dev-версии популярных бесплатных плагинов с маркета, ссылки на их репозитории обычно указаны на странице плагина.

Если плагин отсутствует на маркете, устанавливать его придется вручную. Это нетрудно: нужно просто скачать директорию с плагином и положить в plugins/<authorname>/, где <authorname> — это имя автора плагина (например plugins/graker/blogarchive). И затем нужно запустить миграции командой:

php artisan october:up

Нужно заметить, что October по умолчанию считает плагины, лежащие в plugins, включенными, а october:up нужен только для запуска миграций.

Миграции — это файлы, описывающие создание (и удаление) таблиц БД, необходимых для работы плагина.

Поэтому установленный вручную плагин будет работать и без october:up. Просто необходимые ему таблицы БД не создадутся и при первой же попытке что-либо считать или записать — произойдет ошибка.

Как создать плагин

Самый простой способ — использовать скаффолдинг, то есть с помощью команды artisan:

php artisan create:plugin Author.MyPlugin

Команда создаст в /plugins директорию author (если ее еще нет), а в ней — директорию myplugin с файлами плагина. И да, как вы уже догадались, плагины в OctoberCMS группируются по авторам. Кроме того, нужно иметь в виду, что каждый плагин должен быть в неймспейсе AuthorName\PluginName (с соблюдением регистра).

Структура плагина

В директории с плагином можно найти вот что:

  • assets/ — директория с ассетами (скриптами, картинками, стилями);
  • classes/ — директория со вспомогательными классами, нужными для работы плагина;
  • components/ — компоненты, определенные в плагине;
  • config/ — настройки плагина (вариант «в коде»);
  • controllers/ и models/ — очевидно, контроллеры и модели;
  • console/ — команды artisan;
  • updates/ — миграции и файл version.yaml;
  • widgets/ — описанные в плагине виджеты;
  • Plugin.php — регистрационный файл плагина.

Теперь посмотрим на основные составляющие плагин классы более подробно.

Регистрационный файл

Начнем с конца — с файла Plugin.php. Этот файл содержит основную информацию о том, что вообще в нашем плагине есть: как он называется, какие компоненты, настройки, пункты меню и обработчики событий в нем зарегистрированы и т.д. Поэтому файл Plugin.php называется в документации регистрационным.

Как добавить в плагин компоненты, модели и настройки — я покажу чуть позже. А пока посмотрим на начало регистрационного файла (на примере моего плагина PhotoAlbums).

class Plugin extends PluginBase
{
  /**
   * Returns information about this plugin.
   *
   * @return array
   */
  public function pluginDetails()
  {
    return [
      'name'        => 'PhotoAlbums',
      'description' => 'Create, display and manage galleries of photos arranged in albums',
      'author'      => 'Graker',
      'icon'        => 'icon-camera-retro',
      'homepage'    => 'https://github.com/graker/photoalbums',
    ];
  }
  // ...
}

Это основная информация о плагине: название, описание, автор, иконка (для меню админки, из набора October) и место жительства.

Если наш плагин должен иметь зависимости (от других плагинов), они тоже объявляются в регистрационном файле, в свойстве $require:

public $require = ['RainLab.Blog', 'RainLab.Pages'];

— так мы объявили зависимости от плагинов Blog и Static Pages.

Метод boot()

Метод boot() класса нашего плагина будет вызываться всякий раз перед обработкой входящего запроса. Этот метод хорошо подходит, чтобы, например, прицепиться к событиям, на которые мы хотим реагировать в нашем плагине.

public function boot() {
  Event::listen('backend.form.extendFields', function (Form $widget) {
      // модифицируем $widget
  });
}

В данном примере мы реагируем на событие backend.form.extendFields, которое позволяет нам модифицировать форму, созданную в другом плагине (это не единственный способ модифицировать форму, просто пример).

События в OctoberCMS — это основное средство взаимодействия между плагинами. Можно подслушивать и реагировать на системные события, события, определенные в других плагинах, и конечно генерировать свои события. Подробнее об этом можно почитать в документации.

Настройки и страницы админки

Обычно в плагине объявляется одна или несколько страниц админки с настройками, формами, списками и все такое.

Страницы админки (то есть, бэкэнда) объявляются в регистрационном файле. Вот пример объявления страницы для настроек:

  public function registerSettings()
  {
    return [
      'settings' => [
        'label'       => 'MyPlugin',
        'description' => 'Manage MyPlugin Settings.',
        'icon'        => 'icon-camera-retro',
        'class'       => 'Author\MyPlugin\Models\Settings',
        'order'       => 100,
        'permissions' => ['author.myplugin.access_settings'],
      ]
    ];
  }

В результате в админке, на странице Settings в левой колонке, в секции Other появится пункт MyPlugin, содержащий форму настроек нашего плагина. Состав формы зависит от того, что мы укажем в модели настроек. Для настроек используется модель Author\MyPlugin\Models\Settings, наследующая класс Model и реализующая поведение System.Behaviors.SettingsModel. Хранятся такие настройки в базе данных.

Есть возможность хранить настройки и в коде, но в таком случае для них не генерируется страница админки. И то, и другое описано в документации.

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

  public function registerNavigation()
  {
    return [
      'myplugin' => [
        'label' => 'My Plugin',
        'url' => Backend::url('author/myplugin/mymodels'),
        'icon'        => 'icon-camera-retro',
        'permissions' => ['author.myplugin.manage_mymodels'],
        'order'       => 500,
        'sideMenu' => [
          'new_model' => [
            'label'       => 'New MyModel',
            'icon'        => 'icon-plus',
            'url'         => Backend::url('author/myplugin/mymodels/create'),
            'permissions' => ['author.myplugin.manage_mymodels'],
          ],
          'mymodels' => [
            'label'       => 'MyModels',
            'icon'        => 'icon-copy',
            'url'         => Backend::url('author/myplugin/mymodels'),
            'permissions' => ['author.myplugin.manage_mymodels'],
          ],
        ],
      ],
    ];
  }

В данном примере регистрируется пункт меню админки My Plugin, и два подпункта — New MyModel (форма для создания модели) и MyModels (список существующих моделей). Подпункты будут показаны в боковом меню при выборе My Plugin в главном (верхнем) меню. Причем страница, соответствующая пункту MyModels будет показана по умолчанию (т.к. у этой страницы указан такой же url, как у страницы верхнего уровня).

Также для каждой страницы задается иконка и разрешения, которыми должен обладать пользователь, чтобы он мог зайти на страницу. Вот пример страницы админки для плагина PhotoAlbums (тут определено больше пунктов в сайдбаре):

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

Модели

Одна из основных причин создать новый плагин — это необходимость объявить новый тип данных на сайте и описать работу с ними. Поскольку Laravel, на базе которого сделан October — это MVC-фреймворк, новые типы данных — это модели. Модель — это класс Model из Laravel, реализующий паттерн Active Record. В October класс модели дополнен разными полезностями, такими как генерация формы модели из yaml-файла или возможность приаттачить файл в одну строчку кода.

В рамках создаваемого плагина модели живут в директории models и регистрируются автоматически, так что объявлять их в Plugin.php не нужно. Для каждой модели нужно создать файл models/MyModel.php с классом модели, наследующим класс Model.

Кроме того, в директории models/mymodel/ можно создать два файла: colums.yaml и fields.yaml. Файл columns.yaml описывает поля модели, которые можно вывести в списке моделей:

# ===================================
#  List Column Definitions
# ===================================

columns:
    title:
        label: Title
        searchable: true

    created_at:
        label: Created
        type: date

    updated_at:
        label: Updated
type: date

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

Файл fields.yaml описывает форму, используемую для создания и редактирования модели:

# ===================================
#  Form Field Definitions
# ===================================

fields:
    title:
        label: Title
        span: full
        placeholder: My model title
        required: true

    description:
            label: Description
            type: richeditor
            size: large
            span: right

    image:
        label: Photo
        type: fileupload
        mode: image
        imageWidth: 320
        imageHeight: 240
        span: left

Этот файл также используется контроллером для генерации формы создания/редактирования модели.

Чтобы не создавать файлы и директории вручную, можно просто написать:

php artisan create:model AuthorName.MyPlugin MyModel

и файлы будут сгенерированы.

Миграции

Разумеется, чтобы созданными моделями можно было пользоваться, для них нужно создать таблицы в БД. По умолчанию модель ожидает, что ее таблица будет называться вот так:

authorname_myplugin_mymodels

То есть в нижнем регистре имя автора плагина → подчеркивание → название плагина → подчеркивание → название модели во множественном числе (!). Название таблицы, если очень хочется, можно изменить, сообщив его в свойстве модели.

Чтобы сгенерировать таблицу для модели, нужно создать файл миграции. Если для создания файлов модели мы использовали команду artisan create:model, то файл create_mymodels_table.php будет сгенерирован в директории updates/ автоматически.

Файл миграции выглядит примерно так:

<?php namespace AuthorName\MyPlugin\Updates;
use Schema;
use October\Rain\Database\Updates\Migration;
class CreateMyModelsTable extends Migration
{
  public function up()
  {
    Schema::create('authorname_myplugin_mymodels', function($table)
    {
      $table->engine = 'InnoDB';
      $table->increments('id');
      $table->string('title')->nullable();
      $table->text('description')->nullable();
      $table->integer('user_id')->unsigned()->nullable()->index();
      $table->timestamps();
    });
  }

  public function down()
  {
    Schema::dropIfExists('authorname_myplugin_mymodels');
  }
}

Метод up() будет вызван по команде php artisan october:up и создаст описанную таблицу автоматически. А метод down() предназначен для отката миграции (удаления данных) и вызывается по команде php artisan october:down.

Обновления и версии плагина

Чтобы миграции запустились автоматически при установке плагина, или при выполнении october:up, их нужно описать в файле обновлений, updates/version.yaml:

1.0.1:
    - Initialize plugin
    - create_mymodels_table.php

Соответственно, если мы доработали плагин и наши доработки требуют изменений в БД (например, надо добавить поле в таблицу), то мы создаем еще одну миграцию и добавляем запись в version.yaml:

1.0.1:
    - Initialize plugin
    - create_mymodels_table.php
1.1.0:
    - Add column to mymodels table
    - update_mymodels_table.php

Тогда при запуске october:up миграция из update_mymodels_table.php будет выполнена автоматчески. Также записи в version.yaml нужно добавлять для любых изменений, если плагин живет на маркете — чтобы пользователи могли автоматически обновиться.

Кстати, если нужно полностью (с потерей данных!) переустановить плагин, есть команда plugin:refresh:

php artisan plugin:refresh AuthorName.PluginName

Контроллеры

Чтобы добавленными в плагин моделями можно было управлять (создавать, редактировать и удалять), нам понадобится контроллер. Его можно сгенерировать командой

php artisan create:controller AuthorName.MyPlugin MyModels

Появится файл с классом контроллера controllers/MyModels.php и директория controllers/mymodels/ с файлами представлений (view) и настроек.

Контроллеры и их представления — это по сути кирпичики, из которых (а также из настроек) построен бэк-энд сайта. Как правило, под каждую модель в плагине нужно создать отдельный контроллер.

Контроллеры в October устроены так, чтобы по возможности избавить разработчика от рутинных операций создания списков моделей и форм создания/обновления/удаления. Нужно лишь немного сконфигурировать контроллер и описать свою уникальную бизнес-логику, и админка будет готова. Для примера, сделаем страницу со списком созданных моделей. Что для этого нужно.

Во-первых, нужно сообщить в классе контроллера, что он реализует поведение (behavior) списка и указать, в каком файле лежат настройки списка:

class MyModels extends Controller
{
  public $implement = [
    'Backend.Behaviors.ListController'
  ];
  public $listConfig = 'config_list.yaml';

  // ...
}

Затем создать файл конфига controllers/mymodels/config_list.yaml:

# ===================================
#  List Behavior Config
# ===================================

# Укажем адрес конфига колонок модели (мы его создали выше)
list: $/authorname/myplugin/models/mymodel/columns.yaml

# Укажем класс модели
modelClass: AuthorName\MyPlugin\Models\MyModel

# Заголовок списка
title: Мои модели

# URL для редактирования модели
recordUrl: authorname/myplugin/mymodels/update/:id

# Количество записей на странице
recordsPerPage: 20

# Делаем колонки сортируемыми
showSorting: true

# Тулбар над списком
toolbar:
    # Указываем, где описаны кнопки тулбара
    buttons: list_toolbar

    # Настраиваем поиск
    search:
        prompt: backend::lang.list.search_prompt

Это пример конфига для списка моделей, он не содержит все возможные настройки (они есть в документации), но даже из этого примера получается весьма функциональная страница админки с возможностью выводить модели списком, сортировать по заголовку и дате создания/обновления (как мы написали ранее в columns.yaml), удалять выбранные модели и даже искать модели по названию. Таблица будет сгенерирована вся такая симпатичная (см. картинку выше) и кнопочки/поиски/сортировки из коробки будут работать с AJAX, т.е. без перезагрузки страницы.

Единственное что осталось, чтобы список был виден — привязать его к странице настроек, которую мы ранее зарегистрировали в Plugin.php. Для этого мы в конструкторе нашего контроллера MyModels напишем:

  public function __construct()
  {
    parent::__construct();
    BackendMenu::setContext('Author.MyPlugin', 'myplugin', 'mymodels');
  }

Здесь мы связали контроллер с пунктом меню админки My Plugin, описанным ранее в Plugin.php.

Компоненты

Если контроллеры определяют бэкэнд, то для фронтэнда сайта строительным материалом в October служат компоненты. Компонент — это динамически генерируемый кусочек содержимого сайта. Лента записей в блоге, отдельный пост, верхнее меню, список тегов — это примеры компонентов.

Можно сказать, что компонент — это связующее звено между активной темой сайта (в которой определены все страницы фронт-энда, фрагменты и шаблоны) и плагинами сайта. Именно с помощью компонентов плагин предоставляет активной теме доступ к генерируемому в рамках плагина содержимому.

Как это происходит? Допустим, мы делаем блог и включили плагин Blog. Теперь нам нужно сделать страницу с лентой постов. Мы создаем в теме страницу и прикрепляем к ней компонент Blog Posts, который и рендерит ленту постов. Выглядеть код нашей страницы будет примерно вот так:

title = "Blog"
url = "/blog/:page?"

[blogPosts]
postsPerPage = "10"
==
{% component 'blogPosts' %}

То есть мы просто указываем настройки компонента (в данном случае, количество постов на страницу) и рендерим его с помощью тега {% component 'blogPosts' %}. Все остальное происходит внутри компонента, то есть в плагине Blog. Конечно, скорее всего мы захотим внутри нашей темы изменить дефолтную верстку компонента. Это нетрудно сделать: у каждого компонента есть один или несколько шаблонов, которые нужно просто скопировать в директорию <тема>/partials/<имя компонента>/ и переопределять разметку там. Скопированные файлы подхватятся автоматически.

Теперь посмотрим, как добавить компонент в плагин. Для этого тоже есть удобная команда artisan:

php artisan create:component Author.MyPlugin MyComponent

Команда создаст в директории components файл MyComponents.php с классом компонента, а также файл с шаблоном default.htm в директории components/mycomponent. Файл шаблона содержит результирующую разметку, которая будет вставлена на страницу вместо тега {% component 'mycomponent' %}:

{% set model = __SELF__.model %}

<h1>{{ model.title }}</h1>

{% if model.description %}
    <div class="description row">
        <div class="col-xs-12">
            {{ model.description|raw }}
        </div>
    </div>
{% endif %}

Разумеется, в файле разметки можно обращаться к переменным и функциям, заданным в классе MyComponent. То есть класс компонента выступает контроллером для выводимых нами данных, а шаблон компонента — это представление.

Посмотрим на класс компонента:

<?php namespace Author\MyPlugin\Components;

use Cms\Classes\Page;
use Cms\Classes\ComponentBase;
use Author\MyPlugn\Models\MyModel;

class MyComponent extends ComponentBase
{
  public function componentDetails()
  {
    return [
      'name'        => 'My Model',
      'description' => 'Component to output MyModel'
    ];
  }

  public function defineProperties()
  {
    return [
      'id' => [
        'title'       => 'Id',
        'description' => 'URL id parameter',
        'default'     => '{{ :id }}',
        'type'        => 'string'
      ],
    ];
  }

  // ...

В componentDetails() мы сообщаем имя и описание компонента, а в defineProperties() — задаем свойства (настройки) компонента. Эти настройки появятся в интерфейсе при прикреплении компонента к странице или шаблона, их также будет видно в коде страницы или шаблона в виде текста id = {{ :id }}, как было показано в примере выше. В данном случае у нашего компонента есть свойство id, которое по умолчанию берет id модели для показа из URL страницы.

Благодаря свойствам мы можем использовать один и тот же компонент в разных частях сайта, лишь изменив его свойства.

Теперь посмотрим на продолжение класса компонента. Поскольку компонент выводит на страницу нашу модель MyModel, нам нужно ее загрузить.

  // ...

  public $model;

  public function onRun() {
    $id = $this->property('id');
    $this->model = $this->page->album = MyModel::find($id);
  }
}

Метод onRun() вызывается всякий раз при загрузке страницы и отлично подходит для подготовки данных для показа. Мы получаем id модели из определенного нами свойства компонента, затем загружаем модель по id и сохраняем в свойство $model, чтобы модель была доступна шаблону как __SELF__.model.

В отличие от моделей и контроллеров, чтобы компонент стал доступен, его нужно зарегистрировать в Plugin.php:

public function registerComponents()
{
    return [
        'Author\MyPlugin\Components\MyModel' => 'myModel'
    ];
}

Указанное имя myModel будет использоваться при вставке компонента на страницы.

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

Это все, что я хотел рассказать об устройстве плагинов. Разумеется, в документации можно найти еще больше подробностей. Изложенного здесь, на мой взгляд, достаточно для общего представления о том, как устроен плагин в OctoberCMS.

Спасибо за внимание.

Комментарии