Plugin API
- General structure
- "Sidebar plugins"
- "Event plugins"
- serendipity_plugin_api
- serendipity_property_bag
- add($name, $value)
- type (context: introspect_config_item)
- radio (context: introspect_config_item)
- radio_per_row (context: introspect_config_item)
- select_values (context: introspect_config_item)
- default (context: introspect_config_item)
- validate (context: introspect_config_item)
- validate_error (context: introspect_config_item)
- name (context: introspect_config_item)
- name (context: introspect)
- description (context: introspect_config_item)
- description (context: introspect)
- configuration (context: introspect)
- event_hooks (context: introspect)
- groups (context: introspect)
- requirements (context: introspect)
- cachable_events (context: introspect)
- author (context: introspect)
- version (context: introspect)
- stackable (context: introspect)
- get($name)
- is_set($name)
- add($name, $value)
- serendipity_plugin
- serendipity_event
- Serendipity Framework Variables
- Internationalization
- Building your own plugin
- Using/Adding event hooks
General structure
Each plugin, either sidebar or event plugin, has to be stored inside the plugins subdirectory in a subdirectory. The subdirectory needs to be called either serendipity_event_myplugin or serendipity_plugin_myplugin. Choose no special characters for the subdirectory to not have any OS-specific issues.
Then create an empty PHP file inside that directory and call it like your directory name with the suffix ".php": serendipity_event_myplugin.php, i.e.
It is suggested you just take a look at existing plugins and modify those to get a feel of the structure. Explaining it is much more difficult than actually "learning by doing".
"Sidebar plugins"
Sidebar plugins are plugins that only generate output on the user's frontend. They can take on simple processing logic to create interactivity inside their frontend area, though (like the shoutbox plugin, or the template dropdown).
"Event plugins"
Event plugins are extended sidebar plugins, which do not output immediate data on the user's frontend. Event plugins are a powerful method to tweak s9y's internal power. On each action s9y performs (like displaying an entry, storing an entry, editing an entry) our code has special places where some hooks are placed. On those special places, event plugins can be executed to change the workflow of s9y and to manipulate the data to suit your needs.
Those hooks execute your plugin, which mangles the data (depending on the hook, which can control which data to send), and then returns it to s9y which then normally processes it again.
Currently available hooks are listed in Plugin API - Event hooks.
A event plugin can serve multiple hooks, each of which needs to be registered within the property bag of the plugin.
The s9y Plugin API consists of four classes:
serendipity_plugin_api
The first API class is called serendipity_plugin_api. This is the controlling class which connects s9y to your personal plugin. It is used to load a plugin, to insert a new plugin into (say: "activate") and to remove a plugin. This class often calls methods of the child class it needs to configure, and it is responsible for iterating through the list of available plugins. This API-class is maintained by the s9y developers, and you should not modify or extend the class on your own. See the file serendipity_plugin_api for detailed information of what each method does, if you want to have more insight on it. But you don't need those methods to create your own plugin.
serendipity_property_bag
The second important API class is the serendipity_property_bag. This can be spawned multiple times for any of the plugins. It holds the configuration data and other values of a plugin as well as the general plugin configuration and is the preferred way of storing and retrieving data. The data is stored inside the serendipity_config database table and prefixed with the plugin's unique ID to maintain correlation to the plugin.
The following methods are available:
add($name, $value)
Adds a configuration option called $name to the properties of a plugin. Contents are $value. Common values for $name that are later used for configuration are:
type (context: introspect_config_item)
Sets the type of a configuration option. Possible values are:
- seperator - Acts as a seperating, optical placeholder
- select - Toggles the option to be a dropdown value list [see below for list of values]
- boolean - Toggles the option to be a boolean (TRUE/FALSE) radiobutton
- radio - Toggle the option to be a set of radio buttons [see below for list of values]
- string - Toggles the option to be a string text
- html - Toggles the option to be a large HTML textarea or WYSIWYG-editing interface
- text - Will display a large textarea for plaintext input (no WYSIWYG-editor)
- content - Will show everything of the "default" property attribute. Can be used to pipe raw HTML code from a plugin into the config manager to display virtually any content.
- hidden - A hidden input field
- tristate - A radio button group with three options in a single row
radio (context: introspect_config_item)
Holds an associative array of used values for a "radio" config type. The key of the array is the used option value, the value of the array is the description of the option value shown to the user.
radio_per_row (context: introspect_config_item)
Sets the number of radio buttons shown per row in the plugin configuration manager. Defaults to 2 values per row.
select_values (context: introspect_config_item)
Holds an associative array of used values for a "select" config type. The key of the array is the used option value, the value of the array is the description of the option value shown to the user.
default (context: introspect_config_item)
Holds the default value of any config option when not yet checked.
validate (context: introspect_config_item)
(Available since Serendipity 0.9-ahpa5)
Can validate a specific plugin option when saving. Can hold those types:
- string - Any combination string/numbers, no spaces or punctuation
- words - Any combination string/numbers plus spaces/punctuation
- number - Only numbers
- url - Any URL
- mail - Any E-Mail address
- path - Only characters used for filesystem paths (no spaces, : * ? " < > |)
If you use any other value for this option, it will be applied as a regular expression (i.e. you can use "/[0-9]{4}/" for custom checking)
validate_error (context: introspect_config_item)
(Available since Serendipity 0.9-ahpa5)
Holds a string that is shown in the backend when a config option does not match the validation.
name (context: introspect_config_item)
Sets a name of a configuration option. Is usually filled with a language string constant.
name (context: introspect)
Sets a name of a plugin
description (context: introspect_config_item)
Sets the description of a configuration option. Is usually filled with a language string constant.
description (context: introspect)
Sets the description of a plugin.
configuration (context: introspect)
Holds an array of available configuration options. Also an array if only a single item can be configured!
event_hooks (context: introspect)
Holds an array of registered Event Hooks. Only needed for property bag instances used in Event Plugins.
groups (context: introspect)
Holds an array of valid plugin group constants where the plugin will showup in the backend plugin section. The available groups are as follows:
- FRONTEND_EXTERNAL_SERVICES
- FRONTEND_FEATURES
- FRONTEND_FULL_MODS
- FRONTEND_VIEWS
- FRONTEND_ENTRY_RELATED
- BACKEND_EDITOR
- BACKEND_USERMANAGEMENT
- BACKEND_METAINFORMATION
- BACKEND_TEMPLATES
- BACKEND_FEATURES
- IMAGES
- ANTISPAM
- MARKUP
- STATISTICS
requirements (context: introspect)
Holds an array of required minimum version dependencies. Valid array keys are currently 'serendipity', 'smarty' and 'php'.
cachable_events (context: introspect)
Holds an array of cachable event hooks name, that a certain event plugin can cache. If an event hook is contained, the plugin API will query the $eventData for that hook method for an array key 'is_cached'.
An example may be easier to understand: Let's say an event plugin registers the hook 'frontend_display'. That event hook gets the $eventData array with the contents of the $entry array. Now the plugin can set the key $entry['is_cached'] somewhere in another plugin hook (like 'frontend_fetchentries'). If now the 'frontend_display' hook is called, the plugin API checks if $eventData['is_cached'] is set. Since the plugin DID set that previously, the event hook will not be executed for the plugins.
Thus, this saves a lot of processor cycles. This mechanism is used for the 'entryproperties' plugin a lot, see there for actual code examples.
author (context: introspect)
Holds the name of the plugin author.
version (context: introspect)
Holds the version number(!) of the plugin
stackable (context: introspect)
Holds a boolean value whether a plugin can be installed more than once ("stacking") or not. TRUE means, the plugin can be installed multiple times.
get($name)
Gets a configuration option called $name
is_set($name)
Detects if a configuration option called $name is set for the plugin instance.
serendipity_plugin
This is the core class of both sidebar and event plugins. You use this class to extend your personal plugin to.
Properties
$this->title
Contains the title for a plugin. This is used for automatic plugin introspection and generation of the Spartacus Plugin Repository XML files.
$this->protected (boolean)
Sets whether a plugin can be modified/configured by all users or only the s9y user who inserted this plugin.
$this->wrap_class, $this->$title_class, $this->content_class
Sets the used CSS-wrapper classes for that plugin on output inside the sidebar.
$this->dependencies
Can hold an array list of other plugins that are depending on a plugin. By setting this variable you can create paired plugins, which get created when the partner gets created. This array can have multiple entries.
The key needs to be the name of the depending plugin (string) and the value needs to be either 'remove' or 'keep'. If set to 'remove', the plugin will be removed once the partner plugin is removed. If set to 'keep', the plugin will be unaffected by removing his partner.
Methods:
introspect(&$propbag)
Operates on the property bag to define the properties of your layout. The property bag needs to be filled in this function so that other operations on the plugin can be made. You definitely need to overload this method with a custom one, and then place your configuration elements here.
This is a sample code of how a introspect() function should look in your extended class:
<?php
class serendipity_plugin_myclass extend serendipity_plugin {
function introspect(&$propbag) {
$propbag->add('copyright', 'MIT License');
$propbag->add('name' , 'My first Plugin');
$propbag->add(
'configuration',
array(
'option1',
'option2'
)
);
return true;
}
}
?>
Don't forget about the return part of the plugin, or it will fail to be initialized correctly. Now your myplugin has two configuration options: option1 and option2.
introspect_config_item($name, &$propbag)
This function is needed to tell s9y how to operate on the configuration options. Its input data is the name of the configuration option which should be configured, and will then set the $propbag reference with correct values. Here's an example:
<?php
function introspect_config_item($name, &$propbag)
{
switch($name) {
case 'option1':
$propbag->add('type', 'string');
$propbag->add('name', 'My option1!');
$propbag->add('description', 'Please enter something');
break;
case 'option2':
$propbag->add('type', 'string');
$propbag->add('name', 'My option1!');
$propbag->add('description', 'Please enter something');
break;
}
return true;
}
?>
For the list of available values of 'type' see the description for the 'property_bag' class above.
generate_content(&$title)
This gets handed over an empty reference to the $title value. This function is responsible for setting the $title variable correctly, and then outputting the sidebar data:
<?php
function generate_content(&$title) {
$title = 'All your plugins are belong to us!';
echo 'This is just text, but in the real world here be dragons!';
}
?>
get_config($name, $defaultvalue = null, $empty = true)
Gets configuration data via a s9y interface to fetch data from the database. Will be used in eye-to-eye work with a property bag. Should most likely not loverloaded by you.
set_config($name, $value)
Sets configuration data from a certain config directive via a s9y interface. Will be used in eye-to-eye work with a property bag. Should most likely not loverloaded by you.
install()
This method is executed when a plugin is installed. It's suggested to setup up possible required database tables in this method.
uninstall()
This method is executed when a plugin is removed. You can remove created database tables with this method, if you care.
cleanup()
A function which is called after configuration options have been saved in your s9y backend Administration Suite. This can for example be used to remove outdated temporary data/garbage collection.
register_dependencies($remove = false, $authorid = '0')
Registers dependencies. Evaluates $this->dependencies. Should most likely not loverloaded by you.
example()
Shows output in the plugin configuration section, like the emoticate plugin shows a list of images.
serendipity_event
This class extends the serendipity_plugin class and adds some special methods needed for events.
Methods:
event_hook($event, &$bag, &$eventData)
Reacts to the $event (string, one of the available hooks mentioned above). Operates on the property $bag and can evaluate and alter the referenced $eventData, which is dependant on the input data (also see above).
This function should be overloaded by your plugin to iterate through its actions and operate on the hooks the plugin has registered.
All markup event plugins share a common way of dealing with those hooks and the function like this:
<?php
function event_hook($event, &$bag, &$eventData, $addData = null) {
global $serendipity;
$hooks = &$bag->get('event_hooks');
if (isset($hooks[$event])) {
switch($event) {
case 'frontend_display':
foreach ($this->markup_elements as $temp) {
if (serendipity_db_bool($this->get_config($temp['name'], true)) && isset($eventData[$temp['element']])) {
$element = $temp['element'];
$eventData[$element] = $this->_s9y_markup($eventData[$element]);
}
}
return true;
break;
case 'frontend_comment':
if (serendipity_db_bool($this->get_config(COMMENT, true))) {
echo PLUGIN_EVENT_S9YMARKUP_TRANSFORM . '<br />';
}
return true;
break;
default:
return false;
}
} else {
return false;
}
}
?>
In detail, this means:
- Get the registered hooks off the property bag
- See if the hook which got submitted to the plugin is within our responsibility
- if yes, make a switch() structure and see which hook needs to be evaluated
- each case has the event hookname associated. If one is found:
- Cycle through the list of $this->markup_elements, which holds our configuration directive to where the markup needs to be applied (entry, comments, HTML nugget).
- If the submitted element $eventData matches a config option set to TRUE, we need to apply our markup
- Apply new values to $eventData (remember, this is a reference)
- Return true :-)
Pretty much easier than it seems!
The $addData variable can contain additional data used for the execution of an event hook. Note that this variable is copied by value and not be reference; thus do not make alterations to that variable inside your plugin.
getFieldReference($fieldname, &$eventData)
This method returns the 'body'/'extended' ($fieldname) variable of an $eventData array which contains entries. This is used to ensure interaction with the caching entryproperties plugin - since this one resets the $entry['body'] key to $entry['ep_cache_body'] later on, your plugin should not modify the $entry['body'] key directly, but instead check for existence of the caching plugin. To ease this up, use:
$body = $this->getFieldReference('body', $eventData);
instead, and your plugin will do the magic to apply its functions to the right body/extended array value.
Serendipity Framework Variables
Plugins can make use of Serendipity variables to modify the behavior or output of the Serendipity framework. The defined variables are listed here for reference.
$serendipity
The Serendipity framework uses the $serendipity global associative array to store processing information, plugin information, actions, and just about everything else. The array can be accessed thusly:
<?php global $serendipity; $value = $serendipity['key']; ?>
The keys for the $serendipity array are listed here for reference.
GET
The GET key is an associative array. Its keys correspond to the variables sent to the Serendipity framework through a form using the GET method. The following keys are available, accessible through a call of the form: $serendipity['GET']['key']
action
The 'action' key takes on different values depending on what the user has requested from Serendipity.
The 'action' key may be empty When the user has made no specific request: when displaying the overview page, or redirecting to Serendipity from a non-existent page.
The 'action' key is set to 'read' when viewing single categories, multiple categories, single entries, multiple entries, when previewing a comment, submitting a comment, or accessing an archive page.
The 'action' key is set to 'search' when a search is requested. In this case, the 'searchTerm' will also be available.
adminAction
id
If a single entry has been requested, the 'id' key will be set to the numerical identifier of the entry.
category
The 'category' key is not available unless a single-category or multi-category view has been requested. It is set to a semicolon-separated list of the requested categories.
Note that checking a single box in the category sidebar is still a multi-category request; it's just got a single argument (with no following semicolon).
calendarZoom
The 'calendarZoom' key is not available unless an archive page has been requested. It is set to the epoch date of the requested archive period (the number of seconds since January 1, 1970).
More user-readable information is stored in the 'uriArguments' key of the $serendipity variable.
searchTerm
The 'searchTerm' key is populated to the search argument when the Quick Search? box is used. The arguments are not escaped; they are the plain text typed into the Quick Search? box.
lang
If a language other than the default is specified, its country code is the value for this key. For example, if English is the default language, but a German interface has been specified, $serendipity['lang'] will be set to 'de'.
uriArguments
The 'uriArguments' key is an array of the pseudo-arguments Serendipity uses to determine the page to be displayed. Naturally, its keys and values vary depending on the situation.
archives
The 'archives' value is included in the uriArguments when an archive page is selected. The values proceeding it specify the requested archives; for instance, when archives are configured to display by month, the next two values will be the month and the year.
If no archive period is specified, the overview page is displayed.
categories
The 'categories' value is included in the uriArguments when single or multiple categories have been requested. In the case of a single category, the next value will be the normalized category id-name value. For multiple categories, the next value will specify the requested categories as a semicolon-separated list of category ids followed by the keyword '-multi'.
Internationalization
Avoid hard-coded language strings where possible. Instead just add constants on the top of your plugin file like this:
<?php
switch ($serendipity['lang']) {
case 'de':
@define('PLUGIN_EVENT_TEXTILE_NAME', 'Textformatierung: Textile');
@define('PLUGIN_EVENT_TEXTILE_DESC', 'Textile-Formatierung durchführen');
@define('PLUGIN_EVENT_TEXTILE_TRANSFORM', '<a href="http://www.textism.com/tools/textile/">Textile</a>-Formatierung erlaubt');
break;
case 'en':
default:
@define('PLUGIN_EVENT_TEXTILE_NAME', 'Markup: Textile');
@define('PLUGIN_EVENT_TEXTILE_DESC', 'Parse all output through the Textile converter');
@define('PLUGIN_EVENT_TEXTILE_TRANSFORM', '<a href="http://www.textism.com/tools/textile/">Textile</a>-formatting allowed');
break;
}
?>
If you plan to support multiple languages, you can also include_once 'lang/yourlang.inc.php' seperate files.
Building your own plugin
If you already know what you want to do, try to pick a plugin of our repository which already does something similar. Then edit the file and cut it down to suit your needs using the methods described above.
Save the file inside the right directory structure (see above) and then you can easily insert it inside your s9y Administration Suite.
This example is one of the most basic plugin examples:
<?php
switch ($serendipity['lang']) {
case 'en':
default:
@define('PLUGIN_MYPLUGIN_BLAHBLAH', 'Displays random strings');
@define('PLUGIN_MYPLUGIN_WORDWRAP', 'Wordwrap');
@define('PLUGIN_MYPLUGIN_WORDWRAP_BLAHBLAH', 'How many words until a wordwrap will occur? (Default: 30)');
break;
}
class serendipity_plugin_myplugin extends serendipity_plugin
{
function introspect(&$propbag)
{
global $serendipity;
$propbag->add('name', COMMENTS);
$propbag->add('description', PLUGIN_MYPLUGIN_BLAHBLAH);
$propbag->add('configuration', array('wordwrap'));
}
function introspect_config_item($name, &$propbag)
{
switch($name) {
case 'wordwrap':
$propbag->add('type', 'string');
$propbag->add('name', PLUGIN_MYPLUGIN_WORDWRAP);
$propbag->add('description', PLUGIN_MYPLUGIN_WORDWRAP_BLAHBLAH);
break;
default:
return false;
}
return true;
}
function generate_content(&$title)
{
global $serendipity;
$title = PLUGIN_MYPLUGIN_BLAHBLAH;
$wordwrap = $this->get_config('wordwrap');
if (!is_numeric($wordwrap)) {
$wordwrap = 30;
}
$runs = mt_rand(0,900);
$out = '';
for ($i = 0; $i < $runs; $i++) {
$out .= chr(mt_rand(60,70));
}
echo wordwrap(mt_rand(0, 900), $wordwrap, '<br />', 1);
}
}
?>
Using/Adding event hooks
The usual way of how an event hook is called from within s9y is something like this (example taken from serendipity_entries.php, to display new menu items depending on registered plugins like serendipity_event_statistics):
<?php serendipity_plugin_api::hook_event('backend_sidebar_entries', $serendipity); ?>
So you can see, that's not much of magic. You could easily place the same line of code anywhere else inside the s9y source code, replace backend_sidebar_entries with a different unique ID and then just create an event plugin which registers this event hook.
