This page contains an assortment of problems and their respective solutions related to Grav plugins.
You want to process some custom PHP code, and make the result available in a page.
You create a new plugin that creates a Twig extension, and makes some PHP content available in your Twig templates.
Create a new plugin folder in user/plugins/example
, and add those files:
user/plugins/example/example.php
user/plugins/example/example.yaml
user/plugins/example/twig/ExampleTwigExtension.php
In twig/ExampleTwigExtension.php
you'll do your custom processing, and return it as a string in exampleFunction()
.
Then in your Twig template file (or in a page Markdown file if you enabled Twig processing in Pages), render the output using: {{ example() }}
.
The overview is over, let's see the actual code:
example.php
:
<?php
namespace Grav\Plugin;
use \Grav\Common\Plugin;
class ExamplePlugin extends Plugin
{
public static function getSubscribedEvents()
{
return [
'onTwigExtensions' => ['onTwigExtensions', 0]
];
}
public function onTwigExtensions()
{
require_once(__DIR__ . '/twig/ExampleTwigExtension.php');
$this->grav['twig']->twig->addExtension(new ExampleTwigExtension());
}
}
ExampleTwigExtension.php
:
<?php
namespace Grav\Plugin;
use Grav\Common\Twig\Extension\GravExtension;
class ExampleTwigExtension extends GravExtension
{
public function getName()
{
return 'ExampleTwigExtension';
}
public function getFunctions(): array
{
return [
new \Twig_SimpleFunction('example', [$this, 'exampleFunction'])
];
}
public function exampleFunction()
{
return 'something';
}
}
example.yaml
:
enabled: true
The plugin is now installed and enabled, and it should all just work.
You want to use the taxonomy list Grav plugin to list the tags that are used in your blog posts, but instead of listing all of them, you only want to list the most used items in a given taxonomy (such as the top five tags, for example).
This is an example where the flexibility of Grav plugins really come in handy. The first step is to make sure that you have the taxonomy list Grav plugin installed within your Grav package. After this has been installed, make sure that you copy /yoursite/user/plugins/taxonomylist/templates/partials/taxonomylist.html.twig
to /yoursite/user/themes/yourtheme/templates/partials/taxonomylist.html.twig
as we will be making modifications to this file.
In order to make this work, we are going to introduce three new variables: filter
, filterstart
and filterend
where
true
if we want to be able to list only the top several tags (or whatever other taxonomy you want to use).filterend -1
.The next step will be to make a call to taxonomylist.html.twig
within the template in which we wish to list the top items in our taxonomy. As usual, we will do this using {% include %}
as seen in the following snippet example:
{% if config.plugins.taxonomylist.enabled %}
<div class="sidebar-content">
<h4>Popular Tags</h4>
{% include 'partials/taxonomylist.html.twig' with {'taxonomy':'tag', filter: true, filterstart: 0, filterend: 5} %}
</div>
{% endif %}
In this example, we are going to list the top five tags.
Now, let's turn our attention to taxonomylist.html.twig
. For reference, here is the default code for this file when you initially install it:
{% set taxlist = taxonomylist.get() %}
{% if taxlist %}
<span class="tags">
{% for tax,value in taxlist[taxonomy] %}
<a href="{{ base_url }}/{{ taxonomy }}{{ config.system.param_sep }}{{ tax|e('url') }}">{{ tax }}</a>
{% endfor %}
</span>
{% endif %}
In order to make this work with our new variables (i.e. filter
, filterstart
and filterend
), we will need to include them within this file like so:
{% set taxlist = taxonomylist.get %}
{% if taxlist %}
{% set taxlist_taxonomy = taxlist[taxonomy] %}
{% if filter %}
{% set taxlist_taxonomy = taxlist_taxonomy|slice(filterstart,filterend) %}
{% endif %}
<span class="tags">
{% for tax,value in taxlist_taxonomy %}
<a href="{{ base_url }}/{{ taxonomy }}{{ config.system.param_sep }}{{ tax|e('url') }}">{{ tax }}</a>
{% endfor %}
</span>
{% endif %}
Here, the file is gathering all the items in the taxonomy by default, in a variable called taxlist_taxonomy
.
If filter
has been set, the taxonomy is making use of the slice
Twig filter. This filter will, in our case, extract a subset of an array from the beginning index (in our case, filterstart
) to the ending index (in our case, filterend
).
The for
loop is ran just as it was in the original taxonomylist.html.twig
with the content of taxlist_taxonomy
, filtered or not.
You really like the Grav SimpleSearch plugin, but you want to add a search button in addition to the text field. One reason to add this button is that it may not be readily apparent to the user that they need to hit their Enter
key in order to initiate their search request.
First, make sure that you have installed the Grav SimpleSearch plugin. Next, make sure that you copy /yoursite/user/plugins/simplesearch/templates/partials/simplesearch-searchbox.html.twig
to /yoursite/user/themes/yourtheme/templates/partials/simplesearch-searchbox.html.twig
as we will need to make modifications to this file.
Before we go any further, let's review what this file does:
<input type="text" placeholder="Search..." value="{{ query }}" data-search-input="{{ base_url }}{{ config.plugins.simplesearch.route}}/query" />
<script>
jQuery(document).ready(function($){
var input = $('[data-search-input]');
input.on('keypress', function(event) {
if (event.which == 13 && input.val().length > 3) {
event.preventDefault();
window.location.href = input.data('search-input') + '{{ config.system.param_sep }}' + input.val();
}
});
});
</script>
The first line simply embeds a text input field into your Twig template. The data-search-input
attribute stores the base URL of the resulting query page. The default is http://yoursite/search/query
.
Let's now move onto the jQuery below that. Here, the tag containing the data-search-input
attribute is assigned to a variable input
. Next, the jQuery .on()
method is applied to input
. The .on()
method applies event handlers to selected elements (in this case, the <input>
text field). So, when the user presses (keypress
) a key to initiate the search, the if
statement checks that the following items are true
:
Enter
key has been pressed: event.which == 13
where 13 is the numeric value of the Enter
key on the keyboard.If they are true, then event.preventDefault();
makes sure that the default browser action for the Enter
key is ignored as this would prevent our search from occurring. Finally, the full URL of the search query is constructed. The default is http://yoursite/search/query:yourquery
. From here, /yoursite/user/plugins/simplesearch/simplesearch.php
performs the actual search and the other Twig files in the plugin list the results.
No back to our solution! If we wish to add a search button, we must:
.on()
method to the button, but this time, using click
instead of keypress
This is achieved with the following code using the Turret CSS Framework. Code snippets for other frameworks will be listed at the end.
<div class="input-group input-group-search">
<input type="search" placeholder="Search" value="{{ query }}" data-search-input="{{ base_url }}{{ config.plugins.simplesearch.route}}/query" >
<span class="input-group-button">
<button class="button" type="submit">Search</button>
</span>
</div>
<script>
jQuery(document).ready(function($){
var input = $('[data-search-input]');
var searchButton = $('.button.search');
input.on('keypress', function(event) {
if (event.which == 13 && input.val().length > 3) {
event.preventDefault();
window.location.href = input.data('search-input') + '{{ config.system.param_sep }}' + input.val();
}
});
searchButton.on('click', function(event) {
if (input.val().length > 3) {
event.preventDefault();
window.location.href = input.data('search-input') + '{{ config.system.param_sep }}' + input.val();
}
});
});
</script>
The HTML and class attributes are specific to Turret, but the end result will be something like this. We can also see that the .on()
method has also been assigned to the search button, but it only checks that the number of characters entered into the search box is greater than three before executing the code within the if
statement.
Here is the default HTML for the text field plus a search button for a few other frameworks:
<div class="input-group">
<input type="text" class="form-control" placeholder="Search for...">
<span class="input-group-btn">
<button class="btn btn-default" type="button">Go!</button>
</span>
</div>
<div class="input-field">
<input id="search" type="search" required>
<label for="search"><i class="material-icons">search</i></label>
</div>
<form class="pure-form">
<input type="text" class="pure-input-rounded">
<button type="submit" class="pure-button">Search</button>
</form>
<div class="ui action input">
<input type="text" placeholder="Search...">
<button class="ui button">Search</button>
</div>
You want to access all pages and each page's associated media through PHP and/or Twig, so that they can be looped over or otherwise manipulated by the plugin.
Use Grav's collection-capabilities to construct a recursive index of all pages, and when indexing also gather up media-files for each page. The DirectoryListing-plugin does exactly this, and builds a HTML-list using the produced tree-structure. To do this, we'll create a recursive function - or method as may be the case within a plugin's class - that goes through each page and stores it in an array. The method is recursive, because it calls itself again for each page it finds that has children.
First things first, though, the method takes three parameters: The first is the $route
to the page, which tells Grav where to find it; the second is the $mode
, which tells the method whether to iterate over the page itself or its children; the third is the $depth
, which keeps track of what level the page is on. The method initially instantiates the Page-object, then deals with depth and mode, and constructs the collection. By default, we order the pages by Date, Descending, but you could make this configurable. Then we construct an array, $paths
, to hold each page. Since routes are unique in Grav, they are used as keys in this array to identify each page.
Now we iterate over the pages, adding depth, title, and route (also kept as a value for ease-of-access). Within the foreach-loop, we also try to retrieve child-pages, and add them if found. Also, we find all media associated with the page, and add them. Because the method is recursive, it will continue looking for pages and child-pages until no more can be found.
The returned data is a tree-structure, or multidimensional-array in PHP's parlance, containing all pages and their media. This can be passed into Twig, or used within the plugin itself. Note that with very large folder-structures PHP might time out or fail because of recursion-limits, eg. folders 100 or more levels deep.
/**
* Creates page-structure recursively
* @param string $route Route to page
* @param integer $depth Reserved placeholder for recursion depth
* @return array Page-structure with children and media
*/
public function buildTree($route, $mode = false, $depth = 0)
{
$page = Grav::instance()['page'];
$depth++;
$mode = '@page.self';
if ($depth > 1) {
$mode = '@page.children';
}
$pages = $page->evaluate([$mode => $route]);
$pages = $pages->published()->order('date', 'desc');
$paths = array();
foreach ($pages as $page) {
$route = $page->rawRoute();
$path = $page->path();
$title = $page->title();
$paths[$route]['depth'] = $depth;
$paths[$route]['title'] = $title;
$paths[$route]['route'] = $route;
if (!empty($paths[$route])) {
$children = $this->buildTree($route, $mode, $depth);
if (!empty($children)) {
$paths[$route]['children'] = $children;
}
}
$media = new Media($path);
foreach ($media->all() as $filename => $file) {
$paths[$route]['media'][$filename] = $file->items()['type'];
}
}
if (!empty($paths)) {
return $paths;
} else {
return null;
}
}
Rather than using theme inheritance, it's possible to create a very simple plugin that allows you to use a custom location to provide customized Twig templates.
The only thing you need in this plugin is an event to provide a location for your templates. The simplest way to create the plugin is to use the devtools
plugin. So install that with:
$ bin/gpm install devtools
After that's installed, create a new plugin with the command:
$ bin/plugin devtools newplugin
Fill in the details for the name, author, etc. Say we call it Custom Templates
, and the plugin will be created in /user/plugins/custom-templates
. All you need to do now is edit the custom-templates.php
file and put this code:
<?php
namespace Grav\Plugin;
use \Grav\Common\Plugin;
class CustomTemplatesPlugin extends Plugin
{
/**
* Subscribe to required events
*
* @return array
*/
public static function getSubscribedEvents()
{
return [
'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0]
];
}
/**
* Add current directory to twig lookup paths.
*/
public function onTwigTemplatePaths()
{
$this->grav['twig']->twig_paths[] = __DIR__ . '/templates';
}
}
This plugin simple subscribes to the onTwigTemplatePaths()
event, and then in that event method, it adds the user/plugins/custom-templates/templates
folder to this of paths that Twig will check.
This allows you to drop in a Twig template called foo.html.twig
and then any page called foo.md
will be able to use this template.
NOTE: This will add the plugin's custom template path to the end of the Twig template path array. This means the theme (which is always first), will have precedence over the plugin's templates of the same name. To resolve this, simply put the plugin's template path in the front of the array by modifying the event method:
/**
* Add current directory to twig lookup paths.
*/
public function onTwigTemplatePaths()
{
array_unshift($this->grav['twig']->twig_paths, __DIR__ . '/templates');
}
When developing your own plugins, it's often useful to use Grav's cache to cache data to improve performance. Luckily it's a very simple process to use cache in your own code.
This is some basic code that shows you how caching works:
$cache = Grav::instance()['cache'];
$id = 'myplugin-data'
$list = [];
if ($data = $cache->fetch($id)) {
return $data;
} else {
$data = $this->gatherData();
$cache->save($hash, $data);
return $data;
}
First, we get Grav's cache object, and we then try to see if our data already exists in the cache ($data = $cache->fetch($id)
). If $data
exists, simply return it with no extra work needed.
However, if the cache fetch returns null, meaning it's not cached, do some work and get the data ($data = $this->gatherData()
), and then simply save the data for next time ($cache->save($hash, $data)
).
With the abundance of plugins currently available, chances are that you will find your answers somewhere in their source code. The problem is knowing which ones to look at. This page attempts to list common plugin issues and then lists specific plugins that demonstrate how to tackle them.
Before you proceed, be sure you've familiarized yourself with the core documentation, especially the Grav Lifecycle!
Grav might be flat file, but flat file ≠ static! There are numerous ways read and write data to the file system.
If you just need read access to YAML data, check out the Import plugin.
The preferred interface is via the built-in RocketTheme\Toolbox\File interface.
There's nothing stopping you from using SQLite either.
The simplest example is probably the Comments plugin.
Others include
One way is via the config.plugins.X
namespace. Simply do a $this->config->set()
as seen in the following examples:
You can then access that in a Twig template via {{ config.plugins.X.whatever.variable }}
.
Alternatively, you can pass variables via grav['twig']
:
Finally, you can inject data directly into the page header, as seen in the Import plugin.
According to the Grav Lifecycle, the latest event hook where you can inject raw Markdown is onPageContentRaw
. The earliest is probably onPageInitialized
. You can just grab $this->grav['page']->rawMarkdown()
, munge it, and then write it back out with $this->grav['page']->setRawContent()
. The following plugins demonstrate this:
The latest you can inject HTML, and still have your output cached, is during the onOutputGenerated
event. You can just grab and modify $this->grav->output
.
Many common tasks can be accomplished using the Shortcode Core infrastructure.
The Pubmed and Tablesorter plugins take a more brute force approach.
This is done through the Grav\Common\Assets interface.
You can use PHP's header()
command to set response headers. The latest you can do this is during the onOutputGenerated
event, after which output is actually sent to the client. The response code itself can only be set in the YAML header of the page in question (http_response_code
).
The Graveyard plugin replaces 404 NOT FOUND
with 410 GONE
responses via the YAML header.
The Webmention sets the Location
header on a 201 CREATED
response.
Usually, you'd incorporate other complete libraries into a vendor
subfolder and require
its autoload.php
where appropriate in your plugin. (If you're using Git, consider using subtrees.)
The simplest way is to follow the Custom Twig Filter/Function example in the Twig Recipes section.
Also, read the Twig docs and develop your extension. Then look at the TwigPCRE plugin to learn how to incorporate it into Grav.
Grav provides the Grav\Common\GPM\Response object, but there's nothing stopping you from doing it directly if you so wish.
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.