Writting plugins

New in version 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 :)

Note

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 or COPYING 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

New in version 0.9.

Changed in version 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.

Warning

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.

Note

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"]}

Note

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 for includes directory in your plugin (will be ./plugins/dir_name/includes/dossier for our example)
  • __plugin_templates_dir__ will be replaced with plugin templates 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

New in version 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 and pgsql.sql in order to be found from Galette,
  • update scripts must also follow a naming convention: upgrade-to-{version}-{dbtype}.sql or upgrade-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();

Warning

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

New in version 0.8.3.

Changed in version 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

New in version 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.