Plugins are usually developed because there is a task that can not be completed with Grav's core functionality.
In this tutorial, we will create a plugin that helps Grav to deliver a random page to the user. You have probably seen similar functionality on blog sites as a way to provide a random blog-post when you click a button.
Because there is already a plugin that performs this job named Random
, we'll call this test plugin Randomizer
.
This feature is not possible out-of-the-box, but is easily provided via a plugin. As is the case with a great many aspects of Grav, there is no one-way to do this. Instead, you have many options. We will cover just one approach...
For our plugin we will take the following approach:
Activate the plugin if a URI matches our configured 'trigger route'. (e.g. /random
)
Create a filter so that only configured taxonomies are in the pool of random pages. (e.g. category: blog
)
Find a random page from our filtered pool, and tell Grav to use it for the page content.
OK! This sounds simple enough, right? So, let us get cracking!
Previous versions of this tutorial required creating a plugin manually. This whole process can be skipped thanks to our new DevTools Plugin
The first step in creating a new plugin is to install the DevTools Plugin. This can be done in two ways.
bin/gpm install devtools
For this next step you really do need to be in the command line as the DevTools provide a couple of CLI commands to make the process of creating a new plugin much easier!
From the root of your Grav installation enter the following command:
bin/plugin devtools new-plugin
This process will ask you a few questions that are required to create the 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: [email protected]
SUCCESS plugin Randomizer -> Created Successfully
Path: /www/user/plugins/randomizer
Make sure to run `composer update` to initialize the autoloader
At this point you need to run composer update
in the newly created plugin folder.
The DevTools command tells you where this new plugin was created. This created plugin is fully functional but will not automatically have the logic to perform the function we wish. We will have to modify it to suit our needs.
Now we've created a new plugin that can be modified and developed. Let's break it down and have a look at what makes up a plugin. If you look in the user/plugins/randomizer
folder you will see:
.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── blueprints.yaml
├── randomizer.php
└── randomizer.yaml
This is a sample structure but some things are required:
These items are critical and your plugin will not function reliably unless you include these in your plugin.
blueprints.yaml
- The configuration file used by Grav to get information on your plugin. It can also define a form that the admin can display when viewing the plugin details. This form will let you save settings for the plugin. This file is documented in the Forms chapter.randomizer.php
- This file will be named according to your plugin, but can be used to house any logic your plugin needs. You can use any plugin event hook to perform logic at pretty much any point in Grav's lifecycle.randomizer.yaml
- This is the configuration used by the plugin to set options the plugin might use. This should be named in the same way as the .php
file.These items are required if you wish to release your plugin via GPM.
CHANGELOG.md
- A file that follows the Grav Changelog Format to show changes in releases.LICENSE
- a license file, should probably be MIT unless you have a specific need for something else.README.md
- A 'Readme' that should contain any documentation for the plugin. How to install it, configure it, and use it.As we described in the Plugin Overview, we need to have a few configuration options for our plugin, so the randomizer.yaml
file should look something like this:
enabled: true
active: true
route: /random
filters:
category: blog
This allows us to have multiple filters if we wish, but for now, we just want all content with the taxonomy category: blog
to be eligible for the random selection.
All plugins must have the enabled
option. If this is false
in the site-wide configuration, your plugin
will never be initialized by Grav. All plugins also have the active
option. If this is false
in the site-wide
configuration, each page will need to activate your plugin. Note that multiple plugins also support enabled
/active
in
page frontmatter by using mergeConfig
, detailed below.
The Grav default install has taxonomy defined for category
and tag
by default. This configuration can be modified in your user/config/site.yaml
file.
Of course, as with all other configurations in Grav, it is advised not to touch this default configuration for day-to-day control. Rather, you should create an override in a file called /user/config/plugins/randomizer.yaml
to house any custom settings. This plugin-provided randomizer.yaml
is really intended to set some sensible defaults for your plugin.
The base plugin class structure will already look something like this:
<?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';
}
}
We need to add a few use
statements because we are going to use these classes in our plugin, and it saves space and makes the code more readable if we don't have to put the full namespace for each class inline.
Modify the use
statements to look like this:
use Composer\Autoload\ClassLoader;
use Grav\Common\Plugin;
use Grav\Common\Page\Collection;
use Grav\Common\Uri;
use Grav\Common\Taxonomy;
The two key parts of this class structure are:
namespace Grav\Plugin
at the top of the PHP file.Plugin
appended to the end, and should extend Plugin
, hence the class name RandomizerPlugin
.Grav uses a sophisticated event system, and to ensure optimal performance, all plugins are inspected by Grav to determine which events the plugin is subscribed to.
public static function getSubscribedEvents(): array
{
return [
'onPluginsInitialized' => [
['autoload', 100000], // TODO: Remove when plugin requires Grav >=1.7
['onPluginsInitialized', 0]
]
];
}
In this plugin we are going to tell Grav we're subscribing to the onPluginsInitialized
event. This way we can use that event (which is the first event available to plugins) to determine if we should subscribe to other events.
Note: The first autoload
event listener is only needed in Grav 1.6. Grav 1.7 automatically calls the method.
The next step is to add a method to our RandomizerPlugin
class to handle the onPluginsInitialized
event so it only activates when a user tries to go to the route we have configured in our randomizer.yaml
file. Replace the current 'sample' plugin logic with the following:
public function onPluginsInitialized(): void
{
// Don't proceed if we are in the admin plugin
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]
]);
}
}
First, we get the Uri object from the Dependency Injection Container. This contains all the information about the current URI, including the route information.
The config() method is already part of the base Plugin, so we can simply use it to get the configuration value for our configured route
.
Next, we compare the configured route to the current URI path. If they are equal, we instruct the dispatcher that our plugin will also listen to a new event: onPageInitialized
.
By using this approach, we ensure we do not run through any extra code if we do not need to. Practices like these will ensure your site runs as fast as possible.
The last step of our plugin is to display the random page, and we can do that by adding the following method:
/**
* Send user to a random page
*/
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();
}
}
}
This method is a bit more complicated, so we'll go over what's going on:
First, we get the Taxonomy object from the Grav DI Container and assign it to a variable $taxonomy_map
.
Then we retrieve the array of filters from our plugin configuration. In our configuration this is an array with 1 item: ['category' => 'blog'].
Check to ensure we have filters, then create a new Collection
in the $collection
variable to store our pages.
Append all pages that match the filter to our $collection
variable.
Unset the current page
object that Grav knows about.
Set the current page
to a random item in the collection.
The example plugin that was created with the DevTools plugin, used an event called onPageContentRaw()
. This event is not used in our new plugin, so we can safely remove the entire function.
And that is all there is to it! The plugin is now complete. Your complete plugin class should look something like this:
<?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
*
* The getSubscribedEvents() gives the core a list of events
* that the plugin wants to listen to. The key of each
* array section is the event that the plugin listens to
* and the value (in the form of an array) contains the
* callable (or function) as well as the priority. The
* higher the number the higher the priority.
*/
public static function getSubscribedEvents(): array
{
return [
'onPluginsInitialized' => [
['autoload', 100000], // TODO: Remove when plugin requires Grav >=1.7
['onPluginsInitialized', 0]
]
];
}
/**
* Composer autoload.
*
* @return ClassLoader
*/
public function autoload(): ClassLoader
{
return require __DIR__ . '/vendor/autoload.php';
}
public function onPluginsInitialized(): void
{
// Don't proceed if we are in the admin plugin
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]
]);
}
}
/**
* Send user to a random page
*/
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();
}
}
}
}
If you followed along, you should have a fully functional Randomizer plugin enabled for your site. Just point your browser to the http://yoursite.com/random
, and you should see a random page. You can also download the original Random plugin directly from the Plugins Download section of the getgrav.org site.
One popular technique that is used in a variety of plugins is the concept of merging the plugin configuration (either default or overridden user config) with page-level configuration. This means you can set site-wide configuration, and then have a specific configuration for a given page as needed. This provides a lot of power and flexibility for your plugin.
In recent versions of Grav, a helper method was added to perform this functionality automatically rather than you having to code that logic yourself. The SmartyPants plugin provides a good example of this functionality in action:
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')
));
}
}
Plugins have also the capability of integrating with the bin/plugin
command line to execute tasks. You can follow the plugin CLI documentation if you desire to implement such functionality.
Found errors? Think you can improve this documentation? Simply click the Edit link at the top of the page, and then the icon on Github to make your changes.
Powered by Grav + with by Trilby Media.