Plugin Registration

Introduction

Plugins are the foundation for adding new features to the CMS by extending it. This article describes the component registration. The registration process allows plugins to declare their features such as components or back-end menus and pages. Some examples of what a plugin can do:

  1. Define components.
  2. Define user permissions.
  3. Add settings pages, menu items, lists and forms.
  4. Create database table structures and seed data.
  5. Alter functionality of the core or other plugins.
  6. Provide classes, back-end controllers, views, assets, and other files.

Directory structure

Plugins reside in the /plugins subdirectory of the application directory. An example of a plugin directory structure:

plugins/
  acme/              <=== Author name
    blog/            <=== Plugin name
      assets/
      classes/
      components/
      controllers/
      models/
      updates/
      ...
      Plugin.php     <=== Plugin registration file

Not all plugin directories are required. The only required file is the Plugin.php described below. If your plugin provides only a single component, your plugin directory could be much simpler, like this:

plugins/
  acme/              <=== Author name
    blog/            <=== Plugin name
      components/
      Plugin.php     <=== Plugin registration file

Plugin assets like css and js files must reside under the assets directory:

plugins/
  acme/
    blog/
      assets/        <=== Assets directory
        css/
          styles.css
        js/
          custom.js

Note: if you are developing a plugin for the Marketplace, the updates/version.yaml file is required.

Plugin namespaces

Plugin namespaces are very important, especially if you are going to publish your plugins on the Winter Marketplace. When you register as an author on the Marketplace you will be asked for the author code which should be used as a root namespace for all your plugins. You can specify the author code only once, when you register. The default author code offered by the Marketplace consists of the author first and last name: JohnSmith. The code cannot be changed after you register. All your plugin namespaces should be defined under the root namespace, for example \JohnSmith\Blog.

Registration file

The Plugin.php file, called the Plugin registration file, is an initialization script that declares a plugin's core functions and information. Registration files can provide the following:

  1. Information about the plugin, its name, and author.
  2. Registration methods for extending the CMS.

Registration scripts should use the plugin namespace. The registration script should define a class with the name Plugin that extends the \System\Classes\PluginBase class. The only required method of the plugin registration class is pluginDetails. An example Plugin registration file:

namespace Acme\Blog;

class Plugin extends \System\Classes\PluginBase
{
    public function pluginDetails()
    {
        return [
            'name' => 'Blog Plugin',
            'description' => 'Provides some really cool blog features.',
            'author' => 'ACME Corporation',
            'icon' => 'icon-leaf'
        ];
    }

    public function registerComponents()
    {
        return [
            'Acme\Blog\Components\Post' => 'blogPost'
        ];
    }
}

Supported methods

The following methods are supported in the plugin registration class:

Method Description
pluginDetails() returns information about the plugin.
register() register method, called when the plugin is first registered.
boot() boot method, called right before the request route.
registerComponents() registers any front-end components used by this plugin.
registerFormWidgets() registers any back-end form widgets supplied by this plugin.
registerListColumnTypes() registers any custom list column types supplied by this plugin.
registerMailLayouts() registers any mail view layouts supplied by this plugin.
registerMailPartials() registers any mail view partials supplied by this plugin.
registerMailTemplates() registers any mail view templates supplied by this plugin.
registerMarkupTags() registers additional markup tags that can be used in the CMS.
registerNavigation() registers back-end navigation menu items for this plugin.
registerPermissions() registers any back-end permissions used by this plugin.
registerReportWidgets() registers any back-end report widgets, including the dashboard widgets.
registerSchedule() registers scheduled tasks that are executed on a regular basis.
registerSettings() registers any back-end configuration links used by this plugin.
registerValidationRules() registers any custom validators supplied by this plugin.

Basic plugin information

The pluginDetails is a required method of the plugin registration class. It should return an array containing the following keys:

Key Description
name the plugin name, required.
description the plugin description, required.
author the plugin author name, required.
icon a name of the plugin icon. The full list of available icons can be found in the UI documentation. Any icon names provided by this font are valid, for example icon-glass, icon-music, optional.
iconSvg an SVG icon to be used in place of the standard icon. The SVG icon should be a rectangle and can support colors, optional.
homepage a link to the author's website address, optional.

Routing and initialization

Plugin registration files can contain two methods boot and register. With these methods you can do anything you like, like register routes or attach handlers to events.

The register method is called immediately when the plugin is registered. The boot method is called right before a request is routed. So if your actions rely on another plugin, you should use the boot method. For example, inside the boot method you can extend models:

public function boot()
{
    User::extend(function($model) {
        $model->hasOne['author'] = ['Acme\Blog\Models\Author'];
    });
}

Note: The boot and register methods are not called during the update process to protect the system from critical errors. To overcome this limitation use elevated permissions.

Plugins can also supply a file named routes.php that contain custom routing logic, as defined in the router service. For example:

Route::group(['prefix' => 'api_acme_blog'], function() {

    Route::get('cleanup_posts', function(){ return Posts::cleanUp(); });

});

Dependency definitions

A plugin can depend upon other plugins by defining a $require property in the Plugin registration file, the property should contain an array of plugin names that are considered requirements. A plugin that depends on the Acme.User plugin can declare this requirement in the following way:

namespace Acme\Blog;

class Plugin extends \System\Classes\PluginBase
{
    /**
     * @var array Plugin dependencies
     */
    public $require = ['Acme.User'];

    [...]
}

Dependency definitions will affect how the plugin operates and how the update process applies updates. The installation process will attempt to install any dependencies automatically, however if a plugin is detected in the system without any of its dependencies it will be disabled to prevent system errors.

Dependency definitions can be complex but care should be taken to prevent circular references. The dependency graph should always be directed and a circular dependency is considered a design error.

Extending Twig

Custom Twig filters and functions can be registered in the CMS with the registerMarkupTags method of the plugin registration class. The next example registers two Twig filters and two functions.

public function registerMarkupTags()
{
    return [
        'filters' => [
            // A global function, i.e str_plural()
            'plural' => 'str_plural',

            // A local method, i.e $this->makeTextAllCaps()
            'uppercase' => [$this, 'makeTextAllCaps']
        ],
        'functions' => [
            // A static method call, i.e Form::open()
            'form_open' => ['Winter\Storm\Html\Form', 'open'],

            // Using an inline closure
            'helloWorld' => function() { return 'Hello World!'; }
        ]
    ];
}

public function makeTextAllCaps($text)
{
    return strtoupper($text);
}

Navigation menus

Plugins can extend the back-end navigation menus by overriding the registerNavigation method of the Plugin registration class. This section shows you how to add menu items to the back-end navigation area. An example of registering a top-level navigation menu item with two sub-menu items:

public function registerNavigation()
{
    return [
        'blog' => [
            'label'       => 'Blog',
            'url'         => Backend::url('acme/blog/posts'),
            'icon'        => 'icon-pencil',
            'permissions' => ['acme.blog.*'],
            'order'       => 500,
            // Set counter to false to prevent the default behaviour of the main menu counter being a sum of
            // its side menu counters
            'counter'     => ['\Author\Plugin\Classes\MyMenuCounterService', 'getBlogMenuCount'],
            'counterLabel'=> 'Label describing a dynamic menu counter',
            // Optionally you can set a badge value instead of a counter to display a string instead of a numerical counter
            'badge'       => 'New'

            'sideMenu' => [
                'posts' => [
                    'label'       => 'Posts',
                    'icon'        => 'icon-copy',
                    'url'         => Backend::url('acme/blog/posts'),
                    'permissions' => ['acme.blog.access_posts'],
                    'counter'     => 2,
                    'counterLabel'=> 'Label describing a static menu counter',
                ],
                'categories' => [
                    'label'       => 'Categories',
                    'icon'        => 'icon-copy',
                    'url'         => Backend::url('acme/blog/categories'),
                    'permissions' => ['acme.blog.access_categories'],
                ]
            ]
        ]
    ];
}

When you register the back-end navigation you can use localization strings for the label values. Back-end navigation can also be controlled by the permissions values and correspond to defined back-end user permissions. The order in which the back-end navigation appears on the overall navigation menu items, is controlled by the order value. Higher numbers mean that the item will appear later on in the order of menu items while lower numbers mean that it will appear earlier on.

To make the sub-menu items visible, you may set the navigation context in the back-end controller using the BackendMenu::setContext method. This will make the parent menu item active and display the children in the side menu.

Key Description
label specifies the menu label localization string key, required.
icon an icon name from the Winter CMS icon collection, optional.
iconSvg an SVG icon to be used in place of the standard icon, the SVG icon should be a rectangle and can support colors, optional.
url the URL the menu item should point to (ex. Backend::url('author/plugin/controller/action'), required.
counter a numeric value to output near the menu icon. The value should be a number or a callable returning a number, optional.
counterLabel a string value to describe the numeric reference in counter, optional.
badge a string value to output in place of the counter, the value should be a string and will override the badge property if set, optional.
attributes an associative array of attributes and values to apply to the menu item, optional.
permissions an array of permissions the backend user must have in order to view the menu item (Note: direct access of URLs still requires separate permission checks), optional.
code a string value that acts as an unique identifier for that menu option. NOTE: This is a system generated value and should not be provided when registering the navigation items.
owner a string value that specifies the menu items owner plugin or module in the format "Author.Plugin". NOTE: This is a system generated value and should not be provided when registering the navigation items.

Registering middleware

To register a custom middleware, you can apply it directly to a Backend controller in your plugin by using Controller middleware, or you can extend a Controller class by using the following method.

public function boot()
{
    \Cms\Classes\CmsController::extend(function($controller) {
        $controller->middleware('Path\To\Custom\Middleware');
    });
}

Alternatively, you can push it directly into the Kernel via the following.

public function boot()
{
    // Add a new middleware to beginning of the stack.
    $this->app['Illuminate\Contracts\Http\Kernel']
         ->prependMiddleware('Path\To\Custom\Middleware');

    // Add a new middleware to end of the stack.
    $this->app['Illuminate\Contracts\Http\Kernel']
         ->pushMiddleware('Path\To\Custom\Middleware');
}

Elevated permissions

By default plugins are restricted from accessing certain areas of the system. This is to prevent critical errors that may lock an administrator out from the back-end. When these areas are accessed without elevated permissions, the boot and register initialization methods for the plugin will not fire.

Request Description
/combine the asset combiner generator URL
/backend/system/updates the site updates context
/backend/system/install the installer path
/backend/backend/auth the backend authentication path (login, logout)
winter:up the CLI command that runs all pending migrations
winter:update the CLI command that triggers the update process
winter:env the CLI command that converts configuration files to environment variables in a .env file
winter:version the CLI command that detects the version of Winter CMS that is installed

Define the $elevated property to grant elevated permissions for your plugin.

/**
 * @var bool Plugin requires elevated permissions.
 */
public $elevated = true;

Plugin replacemnet & forking

Plugin replacement is a feature that allows you to create a plugin that replaces (or overrides) another plugin. This is useful when you're forking a plugin to add your own functionality but want to be able to seamlessly migrate from and act as a drop in replacement for the original plugin (i.e. retaining original data, fulfilling other plugin's dependencies on the original plugin, etc).

Registering as a replacement

To enable the plugin replacement feature, specify the identifier for the plugin your plugin is replacing in your plugin details along with the version constraints that define what versions of the plugin are able to be replaced by your plugin.

public function pluginDetails()
{
    return [
        'name'        => 'Acme Plugin',
        'replaces'    => [
            'Acme.Original' => '>=5.0 <=6.0.4'
        ]
    ];
}

Version constraints

Version constraints allow you to restrict your plugin to only override currently installed plugins of specific versions. The above example showcases only replacing a plugin if the original plugin is any version between 5.0 and 6.0.4 inclusive. Most of the time the actual version constraint you'll use will be much simpler, a simple <2.0 to indicate all versions immediately prior to the version you first release your replacement plugin as.

This means you don't have to worry about new versions of the original plugin having changes that may conflict with your changes to the plugin.

Version constraints are specified in the same format that Composer uses. Some valid examples would be:

  • 1.0
  • >=1.0.3
  • <2.0
  • >=1.5.0 <2.0.0
  • self.version

By specifying a version, your plugin will check what version the original plugin is installed at and only if it's version matches the constraint will it disable the original and enable the replacement. If this match fails, then the replacement will be disabled and the original plugin will stay enabled.

Aliases

NOTE: This is for reference only. By registering as a plugin replacement using the above feature Winter already handles registering these aliases throughout the system for you.

Aliasing is a feature of Winter that allows for backwards compatibility and support for inheriting replaced plugins:

  • Configs
  • Lang
  • Settings
  • Navigation

Config

Config supports 2 different types of aliasing: registerNamespaceAlias & registerPackageFallback.

registerNamespaceAlias

This method allows for redirection of the alias to the namespace while accessing config values.

Config::registerNamespaceAlias('winter.replacement', 'winter.original');

For example, register the following config as plugins/winter/replacement/config/config.php:

<?php

return [
    'foo' => 'bar'
];

The config will be accessible via the alias registered:

config('winter.original::foo'); // returns bar
registerPackageFallback

This method allows falling back to an aliased global config (a config specified in /config/acme/plugin/config.php).

Config::registerPackageFallback('winter.replacement', 'winter.original');

The logic to this is as follows:

  • If /config/winter/replacement/config.php exists it will be registered under the winter.replacement namespace.
  • If /config/winter/replacement/config.php does not exist, it will check /config/winter/original/config.php and if found, it will be registered under the winter.replacement.

Lang

Allows for redirection of calls to the alias and returns values from the namespace.

Lang::registerNamespaceAlias('winter.replacement', 'winter.original');

For example, register the following config as plugins/winter/replacement/lang/en/lang.php:

<?php

return [
    'foo' => 'bar'
];

The lang will be accessible via the alias registered:

Lang::get('winter.original::foo'); // returns bar

Settings

There are 2 methods for registering settings aliases. Firstly the aliases can be registered prior to the PluginManager init via lazyRegisterOwnerAlias.

SettingsManager::lazyRegisterOwnerAlias('Winter.Replacement', 'Winter.Original');

If the PluginManager has been loaded, then aliases can be registered via:

SettingsManager::instance()->registerOwnerAlias('Winter.Replacement', 'Winter.Original');

Navigation

There are 2 methods for registering settings aliases. Firstly the aliases can be registered prior to the PluginManager init via lazyRegisterOwnerAlias.

NavigationManager::lazyRegisterOwnerAlias('Winter.Replacement', 'Winter.Original');

If the PluginManager has been loaded, then aliases can be registered via:

NavigationManager::instance()->registerOwnerAlias('Winter.Replacement', 'Winter.Original');

Migrations, seeders and table references

When forking a plugin and using the replace functionality, you will need to handle migratign the data from the original plugin to your replacing plugin via migrations, seeders and the model classes. To do this we recommend the following:

  • Create a migration to rename tables
  • Update models to reference your new table
  • Check migrations for any usage of models

Table renaming

An example migration could look something like this:

<?php namespace Winter\Plugin\Updates;

use Schema;
use Winter\Storm\Database\Updates\Migration;

class RenameTables extends Migration
{
    const TABLES = [
        'example',
        'foo',
        'bar'
    ];

    public function up()
    {
        foreach (self::TABLES as $table) {
            $from = 'acme_plugin_' . $table;
            $to = 'winter_plugin_' . $table;

            if (Schema::hasTable($from) && !Schema::hasTable($to)) {
                Schema::rename($from, $to);
            }
        }
    }

    public function down()
    {
        foreach (self::TABLES as $table) {
            $from = 'winter_plugin_' . $table;
            $to = 'acme_plugin_' . $table;

            if (Schema::hasTable($from) && !Schema::hasTable($to)) {
                Schema::rename($from, $to);
            }
        }
    }
}

Migrations using models

If an old migration (i.e. any migration that runs before the migration that renames the tables) is using a model to populate data, it will be referencing the new table and that will cause issues while updating. The solution to this is dynamically renaming the table before inserting/modifying data:

ExampleModel::extend(function ($model) {
    $model->setTable('acme_plugin_example');
});

// execute seeding code

ExampleModel::extend(function ($model) {
    $model->setTable('winter_plugin_example');
});

If the models use the unique validation rule, you should make sure that the rule is implemented without any modifiers (i.e. just 'slug' => 'unique', not 'slug' => 'unique:winter_plugin_table' so that the call to $model->setTable() will also take effect in that validation logic.

Keep informed

Sign up to our newsletter to receive updates on Winter CMS releases, new features in the works, and much more.
We'll never spam or give this address away.

Latest blog post

October CMS as you know it is Dead

Published April 12, 2021
We regret to inform you that October CMS as you have known it for the past 7 years is no more. The founders have decided to make it a paid proprietary product; unfortunately abandoning the open source community in the process as "source partially available" is not open source. The core maintainers of the project have forked, and will continue development as Winter CMS....

View this post Read all posts

Latest Winter CMS release

v1.1.3

Released April 26, 2021
3 UX/UI Improvements, 19 API Changes, 23 Bug Fixes, 3 Security Improvements, 4 Translation Improvements, 1 Community Improvement, 2 Dependencies

View details View all releases