Plugin Manager
The Plugin Manager module provides a simple user interface and standardized procedures for
- installing,
- uninstalling,
- updating,
- managing and
- integrating
plugins in Admidio.
Make your plugin compatible with the Plugin Manager (Admidio 5)
To make your plugin compatible with the Plugin Manager certain requirements must first be met. These include:
- A consistent folder structure
- Use of the new plugin namespace Plugins
- A JSON configuration file
Naming conventions
To use the new plugin namespace Plugins some naming conventions must be observed:
- No plugin folder or file may contain delimiters such as “-” or “_” especially if it will be used via a class import. For consistency only the optional folder db_scripts (see: folder structure) is exempt from this rule.
- For better readability file and folder names containing multiple words should follow CamelCase notation.
Folder structure
The basic folder structure of any plugin is shown below using the “Birthday” overview plugin as an example:
classes folder
Presenter classes
Plugin-specific classes
Inside the Presenter folder all necessary PagePresenter classes of the plugin are placed. These should be similar to the module-specific PagePresenter classes.
Plugin preferences class
Single FormPresenter element
If the plugin provides preferences there should be a class “[PluginName]PreferencesPresenter”. This class contains a single static preference method following the naming conventions of Admidio's Preferences class.
Below is an example snippet of the createBirthdayForm preference method from the “Birthday” overview plugin:
public static function createBirthdayForm(Smarty $smarty): string { global $gL10n, $gCurrentSession; $pluginBirthday = Birthday::getInstance(); $formValues = $pluginBirthday::getPluginConfig(); $formBirthday = new FormPresenter( 'adm_preferences_form_birthday', $pluginBirthday::getPluginPath() . '/templates/preferences.plugin.birthday.tpl', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/preferences.php', array('mode' => 'save', 'panel' => 'birthday')), null, array('class' => 'form-preferences') ); $selectBoxEntries = array( '0' => $gL10n->get('SYS_DISABLED'), '1' => $gL10n->get('SYS_ENABLED'), '2' => $gL10n->get('ORG_ONLY_FOR_REGISTERED_USER') ); $formBirthday->addSelectBox( 'birthday_plugin_enabled', Language::translateIfTranslationStrId($formValues['birthday_plugin_enabled']['name']), $selectBoxEntries, array( 'defaultValue' => $formValues['birthday_plugin_enabled']['value'], 'showContextDependentFirstEntry' => false, 'helpTextId' => $formValues['birthday_plugin_enabled']['description'] ) ); $formBirthday->addCheckbox( 'birthday_show_names_extern', Language::translateIfTranslationStrId($formValues['birthday_show_names_extern']['name']), $formValues['birthday_show_names_extern']['value'], array('helpTextId' => $formValues['birthday_show_names_extern']['description']) ); [...] $formBirthday->addSubmitButton( 'adm_button_save_birthday', $gL10n->get('SYS_SAVE'), array('icon' => 'bi-check-lg', 'class' => 'offset-sm-3') ); $formBirthday->addToSmarty($smarty); $gCurrentSession->addFormObject($formBirthday); return $smarty->fetch($pluginBirthday::getPluginPath() . '/templates/preferences.plugin.birthday.tpl'); }
Multiple FormPresenter elements
With Admidio 5 the preference page has been redesigned. It is now possible to display multiple “subcards” on one preference panel. To use this feature from a plugin the hasSubcards configuration flag must be set to true.
In your PluginPreferencePresenter class you can now define multiple FormPresenter elements and add different preferences to each form. The first FormPresenter can now be assigned to the smarty variable like the following example shows:
public static function createBirthdayForm(Smarty $smarty): string { [...] $formBirthday = new FormPresenter( 'adm_preferences_form_birthday', $pluginBirthday::getPluginPath() . '/templates/preferences.plugin.birthday.tpl', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/preferences.php', array('mode' => 'save', 'panel' => 'birthday')), null, array('class' => 'form-preferences') ); $selectBoxEntries = array( '0' => $gL10n->get('SYS_DISABLED'), '1' => $gL10n->get('SYS_ENABLED'), '2' => $gL10n->get('ORG_ONLY_FOR_REGISTERED_USER') ); $formBirthday->addSelectBox( 'birthday_plugin_enabled', Language::translateIfTranslationStrId($formValues['birthday_plugin_enabled']['name']), $selectBoxEntries, array('defaultValue' => $formValues['birthday_plugin_enabled']['value'], 'showContextDependentFirstEntry' => false, 'helpTextId' => $formValues['birthday_plugin_enabled']['description']) ); [...] $formBirthday->addSubmitButton( 'adm_button_save_birthday', $gL10n->get('SYS_SAVE'), array('icon' => 'bi-check-lg', 'class' => 'offset-sm-3') ); $formBirthday->addToSmarty($smarty); $gCurrentSession->addFormObject($formBirthday); [...]
Now you can define a new FormPresenter for the second “card”:
$formBirthday2= new FormPresenter( 'adm_preferences_form_birthday_2', $pluginBirthday::getPluginPath() . '/templates/preferences.plugin.birthday.2.tpl', SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/preferences.php', array('mode' => 'save', 'panel' => 'birthday')), null, array('class' => 'form-preferences') ); [...] $formBirthday2->addSubmitButton( 'adm_button_save_birthday_2', $gL10n->get('SYS_SAVE'), array('icon' => 'bi-check-lg', 'class' => 'offset-sm-3') ); [...]
This form cannot be added to smarty with the addToSmarty method because this would override all values set by the previous form element. Therefore a new method appendToSmarty was implemented in the FormPresenter class (currently only available in the plugin-manager branch on GitHub). Use this method to add the second form to the smarty variable:
[...] $formBirthday2->appendToSmarty($smarty); $gCurrentSession->addFormObject($formBirthday2); [..]
You can add as many forms as you like using this method.
When all forms are added to smarty you can use a “wrapper template” to load the form specific template files on one page. For this wrapper template you can add a smarty variable containing informations you want to show. In the following example multiple cards should be used for displaying the different forms:
[...] //assign card titles, icons and corresponding template files $cards = array( array('title' => $birthdayName, 'icon' => $birthdayIcon, 'templateFile' => $birthdayPath . '/templates/preferences.plugin.birthday.tpl'), array('title' => $birthdayName2, 'icon' => $birthdayIcon2, 'templateFile' => $birthdayPath2 . '/templates/preferences.plugin.birthday.2.tpl') ); $smarty->assign('cards', $cards); return $smarty->fetch($birthdayPath . '/templates/preferences.birthday.cards.tpl'); }
The corresponding wrapper template is shown below:
{foreach $cards as $index => $card} <div class="card admidio-tabbed-field-group"> <div class="card-header"><i class="bi {$card.icon} me-1"></i>{$card.title}</div> <div class="card-body"> {include file=$card.templateFile dataIndex=$index} </div> </div> {/foreach}
This template creates cards for each form element defined in the $cards variable and loads the corresponding template file. It is necessary to pass the index of the zero-based $cards array to the corresponding template file to retrieve the correct smarty data that is now also available as a zero-based array for each form variable. These are:
- formType,
- attributes,
- elements,
- javascript and
- hasRequiredFields.
This following example shows a possible implementation of the first form (so with index 0):
<form {foreach $attributes[$dataIndex] as $attribute} {$attribute@key}="{$attribute}" {/foreach}> {include 'sys-template-parts/form.input.tpl' data=$elements[$dataIndex]['adm_csrf_token']} {include 'sys-template-parts/form.select.tpl' data=$elements[$dataIndex]['birthday_plugin_enabled']} {include 'sys-template-parts/form.checkbox.tpl' data=$elements[$dataIndex]['birthday_show_names_extern']} {include 'sys-template-parts/form.select.tpl' data=$elements[$dataIndex]['birthday_show_names']} {include 'sys-template-parts/form.checkbox.tpl' data=$elements[$dataIndex]['birthday_show_age']} {include 'sys-template-parts/form.input.tpl' data=$elements[$dataIndex]['birthday_show_age_salutation']} {include 'sys-template-parts/form.checkbox.tpl' data=$elements[$dataIndex]['birthday_show_notice_none']} {include 'sys-template-parts/form.input.tpl' data=$elements[$dataIndex]['birthday_show_past']} {include 'sys-template-parts/form.input.tpl' data=$elements[$dataIndex]['birthday_show_future']} {include 'sys-template-parts/form.input.tpl' data=$elements[$dataIndex]['birthday_show_display_limit']} {include 'sys-template-parts/form.select.tpl' data=$elements[$dataIndex]['birthday_show_email_extern']} {include 'sys-template-parts/form.select.tpl' data=$elements[$dataIndex]['birthday_roles_view_plugin']} {include 'sys-template-parts/form.select.tpl' data=$elements[$dataIndex]['birthday_roles_sql']} {include 'sys-template-parts/form.select.tpl' data=$elements[$dataIndex]['birthday_sort_sql']} {include 'sys-template-parts/form.button.tpl' data=$elements[$dataIndex]['adm_button_save_birthday']} <div class="form-alert" style="display: none;"> </div> </form> {$javascript[$dataIndex]}
This makes it possible to show seperate forms on the same page. There are no limits to the display options whether as cards (as in the example), tabs, or accordions. With this approach each available Bootstrap/HTML display is possible through the smarty templates.
Entity, Service and ValueObjects classes
If the plugin has specific Entity, Service, or ValueObjects classes these should be placed in the corresponding subfolders as is done with the Admidio modules.
Main plugin class
The main plugin class (e.g.: Birthday) must be placed directly in the classes folder.
db_scripts folder
This folder is optional and is used to place plugin-specific database scripts used when installing (db-install.sql) or uninstalling (db-uninstall.sql) the plugin.
In addition to database scripts update scripts (e.g.: update_1_0.xml) for the plugin can be placed in this folder. These should be structured like the update scripts used by Admidio to update to a new version.
Similar to Admidio's update scripts it is possible to call plugin-specific update steps. To implement these steps a final class named UpdateStepsCode must be present in the folder “[PluginName]\classes\Service\”.
The class implementation should look like this:
final class UpdateStepsCode { /** * @var Database */ private static Database $db; /** * Set the database * @param Database $database The database instance */ public static function setDatabase(Database $database) { self::$db = $database; } /** * @throws Exception */ public static function /*[method name used in update_x_x.xml]*/() { [...] } }
languages folder
This folder contains the plugin-specific translation files.
templates folder
To move away from the deprecated HtmlPage and HtmlTable classes plugins should exclusively use the Smarty template engine with files stored in this folder.
Root plugin folder
Main plugin file
Inside the root plugin folder a main plugin file is required. This file can contain plugin-specific logic (similar to Admidio’s module entry files) or simply initialize the plugin class as in the following example:
<?php use Plugins\Birthday\classes\Birthday; /** *********************************************************************************************** * Birthday * * The plugin lists all users who have birthday in a defined timespan. * * @copyright The Admidio Team * @see https://www.admidio.org/ * @license https://www.gnu.org/licenses/gpl-2.0.html GNU General Public License v2.0 only *********************************************************************************************** */ try { require_once(__DIR__ . '/../../system/common.php'); $pluginBirthday = Birthday::getInstance(); $pluginBirthday->doRender(isset($page) ? $page : null); } catch (Throwable $e) { echo $e->getMessage(); }
Plugin configuration file
Each plugin must provide a JSON configuration file containing basic plugin information and configuration (see: Configuration file).
Configuration file
Each plugin has to provide a JSON configuration file. The following table gives an overview of all keys currently available:
Key: | Mandatory: | Description: |
---|---|---|
“name” | X | Defines the plugin name (can be a translation ID or plain text). |
“description” | A short description of the plugin shown on the Plugin Manager overview (can be a translation ID or plain text). | |
“version” | X | The current version of the plugin. |
“author” | The author of the plugin. | |
“url” | A URL to a wiki page or a project page. | |
“icon” | The icon displayed in the Admidio sidebar and on the Plugin Manager overview. | |
“mainFile” | The filename of the main plugin entry point. If not set “index.php” or “PluginName.php” is assumed. | |
“hasSubcards” | This optional flag is used to determinate whether a plugins preference page contains multiple “cards” to display (for CSS styling purposes only). | |
“dependencies” | (X) | An array of all dependencies the plugin needs to work properly. If this is not set the Plugin Manager cannot detect whether the plugin can be installed properly. |
“defaultConfig” | (X) | An object containing all configuration parameters used by the plugin. Every parameter should contain a “name”, “description”, “type” and “value” key. |
The following example shows the structure based on the “Birthday” overview plugin:
{ "name": "PLG_BIRTHDAY_PLUGIN_NAME", "description": "PLG_BIRTHDAY_PLUGIN_DESCRIPTION", "version": "1.0.0", "author": "Admidio Team", "url": "https://www.admidio.org", "icon": "bi-cake2", "mainFile": "index.php", "dependencies": [ "Admidio\\Infrastructure\\Plugins\\Overview", "Admidio\\Infrastructure\\Plugins\\PluginAbstract", "Admidio\\Infrastructure\\Utils\\SecurityUtils", "Admidio\\Roles\\Service\\RolesService" ], "defaultConfig": { "birthday_plugin_enabled": { "name": "ORG_ACCESS_TO_PLUGIN", "description": "ORG_ACCESS_TO_PLUGIN_DESC", "type": "integer", "value": 1 }, "birthday_show_names_extern": { "name": "PLG_BIRTHDAY_PREFERENCES_SHOW_NAMES_EXTERN", "description": "PLG_BIRTHDAY_PREFERENCES_SHOW_NAMES_EXTERN_DESC", "type": "boolean", "value": false }, "birthday_show_names": { "name": "PLG_BIRTHDAY_PREFERENCES_SHOW_NAMES", "description": "PLG_BIRTHDAY_PREFERENCES_SHOW_NAMES_DESC", "type": "integer", "value": 0 }, "birthday_show_age": { "name": "PLG_BIRTHDAY_PREFERENCES_SHOW_AGE", "description": "PLG_BIRTHDAY_PREFERENCES_SHOW_AGE_DESC", "type": "boolean", "value": false }, "birthday_show_age_salutation": { "name": "PLG_BIRTHDAY_PREFERENCES_SHOW_AGE_SALUTATION", "description": "PLG_BIRTHDAY_PREFERENCES_SHOW_AGE_SALUTATION_DESC", "type": "integer", "value": 18 }, "birthday_show_notice_none": { "name": "PLG_BIRTHDAY_PREFERENCES_SHOW_NOTICE_NONE", "description": "PLG_BIRTHDAY_PREFERENCES_SHOW_NOTICE_NONE_DESC", "type": "boolean", "value": true }, "birthday_show_past": { "name": "PLG_BIRTHDAY_PREFERENCES_SHOW_PAST", "description": "PLG_BIRTHDAY_PREFERENCES_SHOW_PAST_DESC", "type": "integer", "value": 1 }, "birthday_show_future": { "name": "PLG_BIRTHDAY_PREFERENCES_SHOW_FUTURE", "description": "PLG_BIRTHDAY_PREFERENCES_SHOW_FUTURE_DESC", "type": "integer", "value": 2 }, "birthday_show_display_limit": { "name": "PLG_BIRTHDAY_PREFERENCES_SHOW_DISPLAY_LIMIT", "description": "PLG_BIRTHDAY_PREFERENCES_SHOW_DISPLAY_LIMIT_DESC", "type": "integer", "value": 200 }, "birthday_show_email_extern": { "name": "PLG_BIRTHDAY_PREFERENCES_SHOW_EMAIL_EXTERN", "description": "PLG_BIRTHDAY_PREFERENCES_SHOW_EMAIL_EXTERN_DESC", "type": "integer", "value": 0 }, "birthday_roles_view_plugin": { "name": "PLG_BIRTHDAY_PREFERENCES_ROLES_VIEW_PLUGIN", "description": "PLG_BIRTHDAY_PREFERENCES_ROLES_VIEW_PLUGIN_DESC", "type": "array", "value": ["All"] }, "birthday_roles_sql": { "name": "PLG_BIRTHDAY_PREFERENCES_ROLES_SQL", "description": "PLG_BIRTHDAY_PREFERENCES_ROLES_SQL_DESC", "type": "array", "value": ["All"] }, "birthday_sort_sql": { "name": "PLG_BIRTHDAY_PREFERENCES_SORT_SQL", "description": "PLG_BIRTHDAY_PREFERENCES_SORT_SQL_DESC", "type": "string", "value": "DESC" } } }
Plugin class
The Plugin class extends the basic functionality provided by the abstract class PluginAbstract and implements the doRender method.
In its simplest implementation the doRender method can just call the PagePresenter show method:
public static function doRender(?PagePresenter $page = null) : bool { if (!isset($page)) { throw new Exception("The page parameter must be set."); } $page->show(); return true; }
All methods provided by PluginAbstract can be overridden in this plugin class and adapted to the plugin's needs. The following code shows an example override of the getPluginConfig() method in the “Birthday” overview plugin implementation:
public static function getPluginConfig() : array { // get the plugin config from the parent class $config = parent::getPluginConfig(); // if the key equals 'birthday_roles_view_plugin' and the value is still the default value, retrieve the categories from the database if (array_key_exists('birthday_roles_view_plugin', $config) && $config['birthday_roles_view_plugin']['value'] === self::$defaultConfig['birthday_roles_view_plugin']['value']) { $config['birthday_roles_view_plugin']['value'] = self::getAvailableRoles(1, true); } if (array_key_exists('birthday_roles_sql', $config) && $config['birthday_roles_sql']['value'] === self::$defaultConfig['birthday_roles_sql']['value']) { $config['birthday_roles_sql']['value'] = self::getAvailableRoles(1, true); } return $config; }
PluginAbstract class
methods documentation