Écriture de plugins

Nouveau dans la version 0.7.

Depuis les plugins, vous pouvez bénéficier de l’API de Galette, l’étendre ou la compléter avec des classes, vous pouvez créer des pages spécifiques, des entrées de menu et des boutons d’action sur les adhérents.

Le système de plugins a été inspiré par la solution de blogging DotClear.

Un dossier plugins dans Galette héberge les plugins, un dossier par plugin est attendu :

  • plugins
    • Auto
    • Paypal

Tout comme dans Galette, vous trouverez un dossier lang utilisé pour les traductions, un dossier template/default pour les gabarits Smarty, un dossier lib pour les classes, …

Aucun de ces dossiers n’est requis, un plugin peut ne pas en avoir l’utilité :)

Note

Toutes informations de dévelopment de Galette s’appliquent aussi aux plugins. Vous aurez peut-être besoin de d’un peu de déboguage ou de modifier le comportement de l’application

Licence

Les plugins officiels de Galette sont sous licence GPL version 3.

La licence doit :

  • être incluse dans le dossier racine (fichier LICENSE ou COPYING),
  • être présente dans les en-têtes des fichiers sources - si la licence retenue l’exige.

Configuration des plugins

Un fichier ``_define.php``doit être présent pour chaque plugin. Il définit son nom, son auteur, …

<?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'
    ]
);
?>

Si le fichier est manquant ou incorrect, le plugin ne sera pas chargé.

Compatibilité des plugins

Le système de compatibilité des plugins est assez simple : Galette définit une version qui ne change pas à chaque release, et les plugins déclarent une version de Galette compatible. Ces versions sont comparées, et les plugins marqués comme compatibles s’ils déclarent supporter la version de Galette courante.

Du côté de Galette, la version de compatibilité est déclarée avec la constante GALETTE_COMPAT_VERSION dans galette/includes/galette.inc.php. Du côté des plugins, la version de compatibilité est déclarée dans le fichier _define.php du plugin.

Routes

Nouveau dans la version 0.9.

Vous aurez besoin d’URL pour votre plugin. Galette utilise le framework Slim pour exposer ses routes. Chaque URL correspond à une route, avec un nom, de possibles arguments, une méthode HTTP, …

Dans les plugins, vous devez ajouter un fichier _routes.php. Dans ce dernier, vous déclarerez les URL de votre plugin. Galette fournit des URL du type {galette}/plugins/myplugin à laquelle vos propres routes seront ajoutées.

Une routes est constituée des éléments suivants :

  • une URL,
  • éventuellement des paramètres d’URL, dont certains peuvent être requis,
  • un nom (unique),
  • des restrictions d’accès,
  • uen méthode HTTP (GET ou POST).

Un exemple de route ressemblerait à :

<?php
$this->get(
    '/main',
    function ($request, $response) {
        echo 'Welcome to the main page';
    }
)->setName('myplugin_main');

Cela répondra à l’URL {galette}/plugins/myplugin/main ; et affichera simplement Welcome to the main page.

Avertissement

Les noms des routes doivent être uniques. Pour prévenir toute collision, tous les noms de routes doivent être préfixés par le nom du plugin.

Les routes peuvent avoir des paramètres, requis ou non. L’exemple suivant ajoute l’argument requis arg1 et l’argument optionnel arg2 :

<?php
$this->get(
    '/test/{arg1}[/{arg2}]',
    function ($request, $response, $args) {
        //wit an URL like /test/value1/value2
        echo $args['arg1']; //value1
        if (isset($args['arg2'])) {
            echo $args['args2']; /value2
        }
    }
)->setName('monplugin_test');

Il est également possible de restreindre la valeur d’un paramètre en utilisant une expression régulière. Référez-vous à la documentation de routage de Slim pour en savoir plus.

Routes et gabarits

Bien sûr, vous aurez probablement besoin d’autre chose qu’un simple echo d’un point de vue affichage.

Globalement, dans Galette, les routes GET affichent des informations (listes, formulaires, …) et les routes POST exécutent des actions. De cette manière, les formulaires auront une action POST qui fera le travvail, et redirigera ensuite vers la page GET.

Afficher une page depuis un gabarit Smarty ressemblerait à :

<?php
// display page
$this->view->render(
   $response,
   'file:[' . $module['route'] . ']file.tpl', [
       'require_dialog' => true,
       'list_values'    => $myvalues
   ]
);

L’utilisation de $module['root'] vous assure que le fichier que vous chargez est bien celui de votre plugin. Sans cela, si Galette ou un autre plugin fournit un fichier file.tpl, il pourrait être chargé à la place de celui de votre plugin, et ça ne fonctionnera pas. Donc, file.tpl est le file.tpl du cœur, et file:[abcde]file.tpl le fichier file.tpl du plugin dont l’identifiant est abcde.

Note

Galette se charge d’attribuer des identifiants uniques aux plugins. N’essayez pas de le deviner, et utilisez $module['root'] qui est unique par plugin. Le mot clé use sera utilisé pour le passer à vos fonctions anonymes.

$this->get(
    'myplugin_routes',
    function ($request, $response) use ($module) {
        //$module is available here
    }
);

Les redirections sont simples à mettre en œuvre :

<?php
return $response
   ->withStatus(301)
   ->withHeader('Location', $this->router->pathFor('slash'));

Restrictions d’accès

Galette fournit un middleware <https://www.slimframework.com/docs/concepts/middleware.html> qui restreint l’accès aux routes.

Les rôles suivants peuvent être utilisés :

  • superadmin (super-administrator),
  • admin (administrators),
  • staff (membres du bureau)
  • groupmanager (responsables de groupes)
  • member (utilisateur authentifié)

Les rôles groupmanager et member requièrent un peu plus de travail. Une route est accessible pour les responsables de groupe, mais leur accès doit certainement être restreint aux groupes qu’il gèrent.

Pour activer les restrictions d’accès sur une route, appelez le middleware $authenticate à votre route

<?php
$this->get(
    'myplugin_routes',
    function ($request, $response) {
        echo 'Welcome to the main page';
    }
)->setName('myplugin_main')->add($authenticate);

En plus de cela, vous devrez définir l’accès à cette route dans votre fichier _define.php. Dans l’exemple du début de la doc, la route myplugin_main est restreinte aux membres du bureau.

Les pages qui ne nécessitent pas de restrictions particulières ne feront pas appel au middleware. Il en va de même pour les pages qui seraient accessibles à la fois aux utilisateurs authentifiés et non authentifiés. Dans ce cas, la logique de gestion des accès devra se trouver dans votre route et/ou dans vos classes.

Pages publiques

Certaines pages peuvent être accessibles sans authentification, c’est une préférence de Galette. Pour de telles pages, vous devrez vérifier si les pages publiques sont actives pour l’utilisateur courant :

<?php
$this->get(
    '/main',
    function ($request, $response) {
        if (!$this->preferences->showPublicPages($login)) {
            //public pages are not actives
            return $response
                ->withStatus(301)
                ->withHeader('Location', $this->router->pathFor('slash'));
        }
        //content if accessible
    }
)->setName('myplugin_public');

Utilisation

Vous aurez à utiliser des liens vers vos routes, que ce soit dans les gabarits Smarty ou dans les routes elles mêmes (dans le cas d’une redirection par exemple).

Depuis le code PHP, vous utiliserez la méthode pathFor. Si la route attend des paramètres, envoyez les sous forme de tableau associatif :

<?php
$this->router->pathFor('myplugin_main');
$this->router->pathFor('myplugin_test', ['arg1' => 'value1', 'arg2' => 'value2']);

Depuis un gabarit Smarty, utilisez la fonction path_for :

{path_for name="myplugin_main"}
{path_for name="myplugin_test" data=["args1" => "value1", "args2" => "value2"]}

Note

Si un paramètre requis est manquant, le chemin ne sera pas généré et une erreur se produira.

Ressources web

Dans Galette, toutes les ressources qui doivent être accessibles depuis le serveur web (images, fichiers CSS et Javascript) doivent être dans le dossier webroot de votre plugin. Il y aura une sorte de mapping pour que ces ressources soient servies depuis le web.

Smarty

Héritage

Avant Galette 0.9, les gabarits fournissaient une partie de la page seulement, et le code était en charge de l’incorporer dans la page. Mais maintenant, les gabarits doivent déclarer leur héritage.

Trois gabarits parent sont disponibles :

  • page.tpl pour la plupart des pages,
  • public_page.tpl pour les pages publiques,
  • ajax.tpl pour les pages appelées via AJAX.

Les gabarits parents fournissent un bloc content pour afficher le contenu de la page. page.tpl et public_page.tpl fournissent également un bloc javascript pour inclure tous les éléments <script> à l’emplacement adéquat. Aucun de ces bloc n’est obligatoire, mais une page sans contenu n’aurait probablement pas beaucoup de sens ;)

{extends file="page.tpl"}
{block name="content"}
    Your content here
{/block}
{block name="javascript"}
    <script>alert('Hello from javascript.');</script>
{/block}

Le gabarit parent peut être conditionné si vous utilisez une variable :

if $mode eq 'ajax'}
    {assign var="extend" value='ajax.tpl'}
{else}
    {assign var="extend" value='page.tpl'}
{/if}
{extends file=$extend}

Assignation de variables

Il est possible de passer des variables globales à Smarty (avec $tpl->assign('my_var', 'my_value');). Pour cela, ajoutez un fichier _smarties.php à votre plugin plugin. Il ne peut actuellement fournir qu’un seul tableau nommé _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__'
);
?>

Toutes les variables déclarées seront accessibles depuis les gabarits Smarty comme toutes les autres variables : {$my_var}.

Des remplacements automatiques peuvent survenir dans les variables déclarées, en utilisant des chaînes spécifiques :

  • __plugin_include_dir__ cherchera le dossier includes de votre plugin (ce sera ./plugins/dir_name/includes/dossier pour notre exemple)
  • __plugin_templates_dir__ sera remplacé par le dossier templates de votre plugin (ce sera ./plugins/dir_name/templates/ pour notre exemple)
  • __plugin_dir__ sera remplacé par le chemin vers votre plugin (ce sera ./plugins/dir_name/ pour notre exemple)

De cette manière, quel que soit le dossier utilisé, vous trouverez le bon :)

Ajout d’en-têtes HTML

S’il est présent, le contenu du fichier header.tpl sera ajouté aux pages HMTL (dans la balise <head>), juste après celles du cœur.

<link
   rel="stylesheet"
   type="text/css"
   href="{path_for name="plugin_res" data=["plugin" => $module_id, "path" => "galette_pluginname.css"]}"/>

Les en-têtes ajoutés de cette façon seront utilisées dans toute l’application. Pour les fichiers CSS, assurez-vous de en pas changer les règles existantes de Galette, cela pourrait causer des bogues.

Notez également que les chemins vers les fichiers CSS doivent être obtenus en utilisant une route.

Ajout d’actions sur les adhérents

Il est possible pour un plugin d’ajouter des actions sur les adhérents, en ajoutant une ou plusieurs entrées dans la colonne « actions » de la liste des adhérents, ou lors de l’affichage de la fiche membre.

Un fichier adh_actions.tpl dans vos templates ajoutera de nouvelles action dans la liste des adhérents, avec une simple liste de liens :

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

Un autre fichier nommé adh_fiche_actions.tpl dans les templates de votre plugin ajoutera des actions lors de l’affichage d’un adhérent pour modification, sous forme d’éléments de liste (balise li) :

<li>
   <a
      href="{path_for name="myotherroute" data=["id" => $member->id]}"
      id="btn_plugins_myplugin">
      {_T string="Plugin menu entry" domain="myplugin"}
   </a>
</li>

Chaque action ajoutée devra bein spur ajouter du code PHP qui gérera les données envoyées.

Ajout d’actions combinées sur les adhérents

Nouveau dans la version 0.8.

Certaines actions sont prévues pour être lancées en combinaison avec la sélection des adhérents dans la liste, comme les mailings, l’export CSV, la génération des étiquettes, … Il est également possible d’ajouter ce genre d’action depuis un plugin. Créez un fichier adh_batch_action.tpl dans les gabarits de votre plugin, il contiendra des éléments de liste (balise li) avec un bouton de soumission (<input type="submit"/>) :

<li>
    <input type="submit"
        name="pluginname_actionname"
        value="{_T string="My plugin batch action" domain="myplugin"}"
    />
</li>

Déclaration de constantes

Si votre plugin doit créer ses propres tables dans la base de données, il est fortement conseillé d’ajouter un préfixe supplémentaire, de manière à ce que chaque table soit correctement identifiée dans la base de données. Vous déclarerez les constantes dans un fichier _config.inc.php pour cela :

<?php
define('PLUGIN_PREFIX', 'myplugin_');
?>

L’appel à une table dans le code ressemblera alors à :

<?php
[...]
const TABLE = 'mytable';
[...]
// ==> 'SELECT * FROM galette_myplugin_mytable'
$query = 'SELECT * FROM ' . PREFIX_DB . PLUGIN_PREXFIX . self::TABLE;
[...]
?>

Internationalisation

Chaque plugin doit fournir les traduction des nouvelles chaînes qu’il propose. Le système d’internationalisation de Galette s’applique ici. La tâche principale (en dehors de la mise à jour des fichiers durant le développement du plugin) consiste à mettre en place les fichiers de traduction la première fois.

Utilisez un plugin officiel à jour comme référence, et copiez les fichiers lang/Makefile et lang/xgettext.py dans votre propre dossier lang.

$ cd plugins/MyPlugin/lang
$ cp ../../MapsPlugin/lang/Makefile ../../MapsPlugin/lang/xgettext.py .

Vous devrez adapter le fichier Makefile à votre plugin :

  • changer la valeur de DOMAINS pour refléter le(s) domaine(s) de traduction de votre plugin ;
  • changer la valeur de LANGUAGES pour refléter les langues disponibles de votre plugin ;
  • adapter al valeur de PHP_SOURCES.

La variable PHP_SOURCES doit lister tous les fichiers qui peuvent contenir des chaînes à traduire. En fonction de vos besoins et de la hiérarchie de votre plugin, cela peut varier. Par exemple, pour un plugin avec juste quelques classes PHP et quelques gabarits Smarty, vous utiliseriez :

PHP_SOURCES = $(shell find ../ -maxdepth 1 -name \*.php) \
              $(shell find ../lib/GaletteMonPlugin/ -name \*.php) \
              $(shell find ../templates -name \*.tpl)

Si vous respectez les conventions de codage de Galette, vous ne devriez pas avoir à changer PHP_SOURCES. La modification plus avancée du fichier Makefile est hors du cadre de la présente documentation.

Au premier lancement de make, vous verrez pas mal d’erreurs. Vous devriez pouvoir les ignorer, le script n’aime pas travailler avec des fichiers PO vides :) Tous les dossiers requis seront créés, et vous pourrez utiliser votre outil de traduction pour travailler dessus.

Scripts de mise à jour

Dans une nouvelle version, votre plugin sera peut-être amené à ajouter/modifier/supprimer des tables/colonnes/autre dans vos tables. Pour cela, vous devrez créer un dossier scripts. Il sera utilisé de la même manière que {galette}/install/scripts/, et doit suivre les même règles :

  • les scripts d’installation et de mise à jour doivent être fournis pour MariaDB (MySQL) et PosqtgreSQL,
  • les noms des scripts d’installation doivent être mysql.sql et pgsql.sql pour être trouvés depuis Galette,
  • les scripts de mise à jour doivent également suivre une convention de nommage : upgrade-to-{version}-{dbtype}.sql ou upgrade-to-{version}.php, où {version} est la version du plugin et {dbtype} le type de base de données (mysql ou pgsql). Les scripts de mise à jour PHP ne sont pas liés à une moteur de base de données, s’il y a des spécificités, elle devront alors être ajoutées dans le code lui-même.

Le respect de ces règles assure que le plugin sera supporté depuis l’interface de gestion des plugins de Galette, et que les utilisateurs seront en mesure d’installer et de mettre à jour votre plugin facilement.

Classes PHP

Les Plugins peuvent avoir besoin de leurs propres classes. Pour Galette, les nom de classe et espace de noms (namespace) sont importants.

Toutes les classes doivent être dans le dossier lib/{namespace} de votre plugin. Chaque classe est un fichier PHP donc le est celui de la classe (casse comprise). Le namespace est construit avec le nom du plugin tel que déclaré dans _define.php. Dans notre exemple, le nom du plugin est Galette My Plugin et donc le namespace sera GaletteMyPlugin.

La classe MyClass sera écrite dans lib/GaletteMyPlugin/MyClass.php :

<?php
namespace GaletteMyPlugin;

class MyClass {
    [...]
}

Et pour l’appeler :

<?php
[...]
use GaletteMyPlugin\MClass;
$instance = new MyClasse();
//or
$instance = new \GaletteMyPlugin\MyClass();

Avertissement

Lorsque vous utilisez des espaces de noms, toutes les autres bibliothèques et objets de PHP vont faire de même. Dans votre classe MyClass, le nom des classes sera résolu comme suit :

<?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
    }
}

Bibliothèques tierces

Les bibliothèques tierces ne doivent pas être fournies dans les sources du plugin, mais dans les release seulement.

Galette utilise composer pour gérer ses bibliothèques tierces, les plugins peuvent faire de même au besoin.

Système de fichiers

Finalement, un dossier de plugin ressemblerait à :

  • plugins
    • galette-myplugin
      • includes
      • lang
      • lib
        • GaletteMyPlugin
      • templates
        • default
          • headers.tpl
          • menu.tpl
      • webroot
          • images
      • _config.inc.php
      • _define.php
      • _smarties.php
      • _routes.php

Et pour toutes les questions que vous pourriez encore vous poser au sujet du développement… Hé bien, référez-vous au manuel PHP, manuel Smarty, un client courriel pour écrire aux listes de diffusion, et potentiellement d’un client IRC pour rejoindre le canal IRC Galette ;-)

Tout comme le code source de Galette, les plugins doivent suivre les conventions de codage PSR2 : https://www.php-fig.org/psr/psr-2/

Puisque Galette supporte à la fois MariaDB et PostgreSQL, il serait logique que les plugins fassent de même.

Formulaire d’enregistrement

Nouveau dans la version 0.8.3.

Modifié dans la version 0.9.

Il est possible de reconfigurer le formulaire d’adhésion. Une version basique est fournie par Galette, qui utilise les modèles PDF, mais cela peut ne pas convenir à tous. Le plugin fullcard par exemple, va remplacer par sa propre version, sans changement dans l’URL du navigateur (entièrement invisible pour les utilisateurs).

Ceci est activé en créant un fichier _preferences.php dans votre plugin, avec un contenu du genre :

<?php
$_preferences = [
    'pref_adhesion_form' => '\GaletteFullcard\PdfFullcard'
];