Writting plugins¶
Нове в версії 0.7.
From plugins, you can benefit from the entire Galette API, extends or complete it with classes, you can create specific pages, menu entries, and action buttons on members.
Plugin system was inspired from DotClear blogging solution.
A plugins
directory in Galette will host plugins, one directory per plugin is expected:
plugins
Auto
Paypal
…
Just as in Galette, you will find a lang
directory used for translation files, a template/default
directory for Smarty templates, a lib
directory for classes, …
None of those directories are mandatory, plugin may have no need for them :)
Примітка
A migration guide for plugins from Galette 0.8 to 0.9 is available.
Примітка
All Galette development information also apply to plugins. You may need to debug a bit or change application behavior
License¶
Official Galette plugins are licensed under GPL version 3.
License must:
- be included in the root directory (
LICENSE
orCOPYING
file), - be present in all source file headers - if the selected license wants it.
Plugins Configuration¶
A _define.php
file must be present for each plugin. It defines plugin name, its author, …
<?php
$this->register(
'Galette My Plugin', //Name
'Plugin that does nothing', //Short description
'Your name', //Author
'0.0.1', //Version
'0.9', //Galette version compatibility
'myplugin', //routing name and translation domain
'2019-10-04', //Release date
[ //Permissions needed
'myplugin_main' => 'staff'
]
);
?>
If the file is missing or incorrect, plugin will not be loaded at all.
Plugins compatibility¶
Plugins compatibility is a quite simple system: Galette define a compatibility version that does not change on every Galette release, and plugins declare a Galette compatible version. Those versions are compared, and plugin is marked as compatible if it declare to support current Galette version.
On Galette side, compatibility version is declared with GALETTE_COMPAT_VERSION constant in galette/includes/galette.inc.php
.
On plugin side, compatibility version is declared in the _define.php
plugin file.
Routes¶
Нове в версії 0.9.
Змінено в версії 0.9.5.
You will need some URLs for your plugin. Galette rely on Slim framework to expose routes. Each URL fit a route, with a name, possible arguments, HTTP method, …
In plugins, you must add a _routes.php
file. In this file, you will declare all your plugin URLs. Galette provide URL similar to {galette}/plugins/myplugin
on which your own routes wil be append.
A route is constitued of the following elements:
- an URL,
- maybe some URL parameters, some may be required,
- a controller class and method to be called,
- a name (unique),
- access restriction,
- a HTTP method (GET and/or POST).
A simple route example would look like:
<?php
$this->get(
'/main',
[TheController::class, 'welcome']
)->setName('myplugin_main');
And the corresponding method in controller would look like:
<?php
public function welcome(Request $request, Response $response): Response
{
$response->getBody()->write('Welcome to the main page');
return $response;
}
This will respond to the URL {galette}/plugins/myplugin/main
; and it will just display Welcome to the main page.
Попередження
Routes names must be unique. To prevent any collision, all plugins routes names must be prefixed with plugin name.
Routes can have parameters, mandatory or not. Following example add the arg1 required parameter, and the arg2 optionnal one:
<?php
$this->get(
'/test/{arg1}[/{arg2}]',
[TheController::class, 'test']
)->setName('monplugin_test');
And the corresponding method in controller would look like:
<?php
public function test(Request $request, Response $response, int $arg1, string $arg2 = null): Response
{
//with an URL like /test/1/value2
$response->getBody()->write(
$arg1 . //1
' ' .
$arg2 ?? '' //value2
); //1 value2
return $response;
}
It is also possible to restrict a parameter value using regular expressions. See Slim routing documentation to know more.
Controller¶
As we’ve just seen; you have to create at least one controller class which inerits from Galette\Controllers\AbstractPluginController
and contains a $module_info
class parameter used to inject plugins information.
<?php
namespace GaletteMaps\Controllers;
use Galette\Controllers\AbstractPluginController;
use Psr\Container\ContainerInterface;
use Slim\Http\Request;
use Slim\Http\Response;
/**
* MyPlugin controller
*
* @category Controllers
* @name TheController
* @package Galette
* @author Johan Cwiklinski <johan@x-tnd.be>
* @copyright 2021 The Galette Team
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
* @link https://galette.eu
*/
class TheController extends AbstractPluginController
{
/**
* @Inject("Plugin Galette My Plugin")
* @var integer
*/
protected $module_info;
}
Routes and templates¶
Of course, you will probably need something more than simple echo
from a display point of view;
Globally, inside Galette, GET routes displays information (lists, forms, …) and POST routes do actions. That way, forms will have a POST action, that will do the job, and then will redirect on a GET page.
Displaying a page from a Smarty template would look like:
<?php
// display page
$this->view->render(
$response,
'file:[' . $this->getModuleRoute() . ']file.tpl', [
'require_dialog' => true,
'list_values' => $myvalues
]
);
The use of the $this->getModuleRoute()
ensures the file you are trying to load is the one from your plugin. Without that, if Galette or another plugin provides a file.tpl file, it may be loaded instead of the one from your plugin, and this won’t work.
Then, file:file.tpl
is core template file, while file:[myplugin]file.tpl
the template from plugin which identifier is myplugin
.
Примітка
Galette is in charge to attribute identifiers to plugins. Do no try to guess it, and use $this->getModuleRoute()
which is unique per plugin.
Redirections are simple to do:
<?php
return $response
->withStatus(301)
->withHeader('Location', $this->router->pathFor('slash'));
Access restrictions¶
Galette provides a middleware <https://www.slimframework.com/docs/concepts/middleware.html> which restricts routes access.
Following roles can be used:
superadmin
(super-administrator),admin
(administrators),staff
(staff members)groupmanager
(groups managers)member
(logged in user)
groupmanager
and member
roles requires additional work. A route that is accessible for groups managers, but their access must certainly be restricted to the groups they owns.
To add a restriction access to a route, call the $authenticate
middleware on your route:
<?php
$this->get(
'myplugin_routes',
[TheController::class, 'welcome']
)->setName('myplugin_main')->add($authenticate);
Along with that, you have to define the access to that route in your _define.php
file. In the example from the begginning of the doc, myplugin_main
route has been restricted to staff members only.
Pages which does not need any specific restriction will just not call the middleware. It is the same for pages which may be displayed for boths logged in and not. In that case, you must have logic in your route and/or in your classes to manage access.
Public pages¶
Some of pages may be accessible without authentication, this is a Galette preference. For such pages, you will have to check if public pages are active for current logged in user in controller method:
<?php
public function welcome(Request $request, Response $response): Response
{
if (!$this->preferences->showPublicPages($login)) {
//public pages are not actives
return $response
->withStatus(301)
->withHeader('Location', $this->router->pathFor('slash'));
}
//content if accessible
return $response;
}
Usage¶
You will need to use links to your different routes, either in Smarty templates or in routes themselves (redirection case for example).
From PHP code, you will use pathFor
method. If route is waiting for parameters, send them as an indexed array:
<?php
$this->router->pathFor('myplugin_main');
$this->router->pathFor('myplugin_test', ['arg1' => 1, 'arg2' => 'value2']);
From a Smarty template, use the path_for
function:
{path_for name="myplugin_main"}
{path_for name="myplugin_test" data=["arg1" => 1, "arg2" => "value2"]}
Примітка
If a required parameter is missing, path will not be generated and this will produce an error.
Web resources¶
In Galette, all resources that must be read from the server (images, CSS and javascript files) must be in the webroot
directory in your plugin. This one will be kind of mapped to be served from the web.
Smarty¶
Heritage¶
Before Galette 0.9, templates was providing a page part only, and PHP code was in charge to include it in the page. But now, template files must declare their heritage.
Three parent templates are provided:
page.tpl
for most of the pages,public_page.tpl
for public pages,ajax.tpl
for AJAX called pages.
Parents templates provide a content
block to display page contents. page.tpl
and public_page.tpl
also provide a javascript
to include all <script>
elements at the right place. None of those blocks is mandatory, but an empty page would not make sense ;)
{extends file="page.tpl"}
{block name="content"}
Your content here
{/block}
{block name="javascript"}
<script>alert('Hello from javascript.');</script>
{/block}
Parent template can be conditionned if you use a variable:
if $mode eq 'ajax'}
{assign var="extend" value='ajax.tpl'}
{else}
{assign var="extend" value='page.tpl'}
{/if}
{extends file=$extend}
Variables assignement¶
It is possible to pass global variables to Smarty (with $tpl->assign('my_var', 'my_value');
). To achieve that, add a _smarties.php
file to your plugin. It may currently provide only one array named _tpl_assignments
:
<?php
$_tpl_assignments = array(
'my_var' => 'my_value',
'dossier_includes' => '__plugin_include_dir__dossier',
'nomplugin_tpl_dir' => '__plugin_templates_dir__',
'nomplugin_dir' => '__plugin_dir__'
);
?>
All declared variables will be accessible from Smarty templates like all other variables: {$my_var}
.
Automatic replacements may occurs in declared variable, using specific strings:
__plugin_include_dir__
will look forincludes
directory in your plugin (will be./plugins/dir_name/includes/dossier
for our example)__plugin_templates_dir__
will be replaced with plugintemplates
directory (will be./plugins/dir_name/templates/
for our example)__plugin_dir__
will be replaced with path to your plugin (will./plugins/dir_name/
for our exemple)
That way, whatever the directory name used, you’ll find the good one :)
Add HTML headers¶
When present, the content of header.tpl
file will add its content in HTML pages headers (the <head>
tag), just after core ones.
<link
rel="stylesheet"
type="text/css"
href="{path_for name="plugin_res" data=["plugin" => $module_id, "path" => "galette_pluginname.css"]}"/>
Headers added this way will be used in the entire application. For CSS stylesheet files, please make sure not to change existing Galette rules, this may cause display issues.
Also note the path to the CSS file must be obtained using a route.
Add actions on members¶
It is possible for a plugin to add actions on members, adding one or more entries in members list «actions» column, or displaying one member information.
An adh_actions.tpl
file in your plugin templates will add new actions in members list, with a simple list of links:
<a href="{path_for name="myroute" data=["id" => $member->id]}">
<img
src="{path_for name="plugin_res" data=["plugin" => $module_id, "path" => "images/icon-plugin.png"]}"
alt="{_T string="Plugin menu entry" domain="myplugin"}"
width="16" height="16"/>
</a>
Another file named adh_fiche_action.tpl
in your plugin templates will add actions displaying a member for edition, as a HTML list element (li
tag):
<li>
<a
href="{path_for name="myotherroute" data=["id" => $member->id]}"
id="btn_plugins_myplugin">
{_T string="Plugin menu entry" domain="myplugin"}
</a>
</li>
Each added action must of course add a PHP code that will handle sent data.
Add combined actions on members¶
Нове в версії 0.8.
Some actions are available to be run combined with a members selection from the list, like mailings, CSV exports, labels generations, … It is also possible to add that kind of action from a plugin. Create a adh_batch_action.tpl
file in your plugin templates, it will contain a HTML list element (li
tag) with a send button (<input type="submit"/>
):
<li>
<input type="submit"
name="pluginname_actionname"
value="{_T string="My plugin batch action" domain="myplugin"}"
/>
</li>
Constants declaration¶
If your plugin must own his own tables in database, it is adivsed to declare an extra prefix so each table can be easily identified in the database.
You can declare constants in a _config.inc.php
file to achieve that:
<?php
define('PLUGIN_PREFIX', 'myplugin_');
?>
Call to a table in the code will then look like:
<?php
[...]
const TABLE = 'mytable';
[...]
// ==> 'SELECT * FROM galette_myplugin_mytable'
$query = 'SELECT * FROM ' . PREFIX_DB . PLUGIN_PREXFIX . self::TABLE;
[...]
?>
Internationalisation¶
Every plugin must provide translations for new string it proposes. Galette global internationalisation system applies here. The main task (exepted files update while developing plugin) consists to set up translation files the first time.
Use an official plugin up to date as references, and copy lang/Makefile
and lang/xgettext.py
files in your own lang
directory:
$ cd plugins/MyPlugin/lang
$ cp ../../MapsPlugin/lang/Makefile ../../MapsPlugin/lang/xgettext.py .
You will have to adapt Makefile
file to your plugin:
- change
DOMAINS
value to reflect translation(s) domain(s) of your plugin; - change
LANGUAGES
value to reflect available langs of your plugin; - adapt
PHP_SOURCES
value.
PHP_SOURCES
variables will list all files that mays contains strings to translate. Regarding your needs and your plugins directory hierarchy; they may vary. For example, for a plugin with only a few PHP classes and some Smarty templates, you would use:
PHP_SOURCES = $(shell find ../ -maxdepth 1 -name \*.php) \ $(shell find ../lib/GaletteMonPlugin/ -name \*.php) \ $(shell find ../templates -name \*.tpl)
If you follow Galette development standards, you should not have to change PHP_SOURCES
. Advanced editing of the Makefile
is out of the gaols of the documentation.
First time you will launch make, you may see a lot of errors. You should ignore them, the script is not happy to work with empty PO files :) All required directories and files will be created, and you can now use your translation tool to work on them.
Update scripts¶
In a new version, your plugin may need to add/change/drop new tables/columns/else in your tables. To achieve that, you must create a scripts
directory. It is handled the exact same ay as {galette}/install/scripts/
, and must follow the same rules:
- installation and update scripts must be provided for both MariaDB (MySQL) and PostgreSQL,
- installation script names must be
mysql.sql
andpgsql.sql
in order to be found from Galette, - update scripts must also follow a naming convention:
upgrade-to-{version}-{dbtype}.sql
orupgrade-to-{version}.php
, where {version} is the new plugin version and {dbtype} the database type (mysql or pgsql). PHP update scripts does not rely on database engine, if there are specificities, they’ll be handled in code itself.
Respecting those rules ensures plugin will be supported from the Galette plugins management interface, and user will be able to install or update easily your plugin.
PHP classes¶
Plugins may need their own classes. For Galette, class name and namespace (namespace) are importants.
All classes must be in the lib/{namespace}
directory of your plugin. Each class is a PHP file which name is the class name (including case). Namespace is built with plugin name as declared in _define.php
. In our example, plugin name is Galette My Plugin
and therefore the namespace will be GaletteMyPlugin
.
The MyClass class will will be written in lib/GaletteMyPlugin/MyClass.php
:
<?php
namespace GaletteMyPlugin;
class MyClass {
[...]
}
And to call it:
<?php
[...]
use GaletteMyPlugin\MClass;
$instance = new MyClasse();
//or
$instance = new \GaletteMyPlugin\MyClass();
Попередження
When you use namespaces, all other libraries or PHP objects used them aswell. In your MyClass
, names of classes will be resolved that way:
<?php
namespace GaletteMyPlugin;
class MyClass {
public myMethod() {
$object = new stdClass(); // ==> instanciate a \GaletteMyPlugin\stdClass() - that does not exists
$otherobject = new \stdClass(); // ==> instanciate a PHP stdClass object
}
}
Third party libraries¶
Third party dependencies must not be included in plugin sources, but in releases only.
Galette uses composer to handle third party libraries, plugins can do the same if needed.
File system hierarchy¶
Finally, a plugin directory should look like:
plugins
galette-myplugin
includes
…
lang
…
lib
GaletteMyPlugin
Controllers
TheController.php
…
templates
default
headers.tpl
menu.tpl
…
webroot
…
images
…
_config.inc.php
_define.php
_smarties.php
_routes.php
…
And for all remaining development questions… Well, rely on PHP manual, Smarty manual, a mail client to write to mailing lists, and potentially an IRC client to join Galette IRC channel ;-)
Just like Galette core source code, plugins must follow PSR2 coding standards: https://www.php-fig.org/psr/psr-2/
Since Galette provide support for both MariaDB and PostgreSQL, it would be logicial for plugins to do the same.
Registration form¶
Нове в версії 0.8.3.
Змінено в версії 0.9.
It is possible to reconfigure the registration form. A basic version is provided in Galette, that uses PDF models, but it may not suit everyone needs. The fullcard plugin for example, will override to provide its own version, without any change in the browsers URL (completely invisible for users).
This is enabled by creating a _preferences.php
file in your plugin, with a content like:
<?php
$_preferences = [
'pref_adhesion_form' => '\GaletteFullcard\PdfFullcard'
];
Galette events¶
Нове в версії 0.9.4.
Galette emit some events when members, contributions and transactions are added, updated or removed. This is provided using PHP league Event library.
All possible events are:
adherent.add
,adherent.edit
,adherent.remove
,contribution.add
,contribution.edit
,contribution.remove
,transaction.add
,transaction.edit
,transaction.remove
.
In order to catch any of those events, you will need a PHP class named PluginEventProvider
in your plugin namespace, which must provide a provideListeners
method:
<?php
namespace GaletteMyPlugin;
use League\Event\ListenerAcceptorInterface;
use League\Event\ListenerProviderInterface;
use Analog\Analog;
class PluginEventProvider implements ListenerProviderInterface
{
public function provideListeners(ListenerAcceptorInterface $acceptor)
{
$acceptor->addListener('member.add', function ($event, $member) {
Analog::log(
sprintf(
'[%1$s] Event emitted: member %2$s has been added.',
get_class($this),
$member->sfullname
),
Analog::DEBUG
);
});
}
}
First argument of your listener is the event name, and the second an anonymous function that will receive the event itself as first argumennt, and an instance of the related Galette object. You can of course add several listeners on possible events.