Plugin development tutorialยถ

This tutorial explores the basic concepts of GLPI while building a simple plugin. It has been written to be explained during a training session, but most of this document could be read and used by people wanting to write plugins. Donโ€™t hesitate to suggest enhancements or contribute at this address: https://github.com/glpi-project/docdev

Warning

โš ๏ธ Several prerequisites are required in order to follow this tutorial:

  • A base knowledge of GLPI usage

  • A correct level in web development:

    • PHP

    • HTML

    • CSS

    • SQL

    • JavaScript (JQuery)

  • Being familiar with command line usage

๐Ÿ“ In this first part, we will create a plugin named โ€œMy pluginโ€ (key: myplugin). We will cover project startup as well as the setup of base elements.

Prerequisitesยถ

Here are all the things you need to start your GLPI plugin project:

  • a functional web server,

  • latest GLPI stable release installed locally

  • a text editor or any IDE (like vscode or phpstorm),

  • git version management software.

You may also need:

  • Composer PHP dependency software, to handle PHP libraries specific for your plugin.

  • Npm JavaScript dependency software, to handle JavaScript libraries specific for your plugin.

Start your projectยถ

Warning

โš ๏ธ If you have production data in your GLPI instance, make sure you disable all notifications before beginning the development. This will prevent sending of tests messages to users present in the imported data.

First of all, a few resources:

  • Empty plugin and its documentation. This plugin is a kind of skeleton for quick starting a brand new plugin.

  • Example plugin. It aims to do an exhaustive usage of GLPI internal API for plugins.

My new pluginยถ

Clone empty plugin repository in you GLPI plugins directory:

cd /path/to/glpi/plugins
git clone https://github.com/pluginsGLPI/empty.git

You can use the plugin.sh script in the empty directory to create your new plugin. You must pass it the name of your plugin and the first version number. In our example:

cd empty
chmod +x plugin.sh
./plugin.sh myplugin 0.0.1

Note

โ„น๏ธ Several conditions must be respected choosing a plugin name: no space or special character is allowed.
This name will be used to declare your plugin directory, as well as methods, constants, database tables and so on.
My-Plugin will therefore create the MyPlugin directory.
Using capital characters will cause issues for some core functions.

Keep it simple!

When running the command, a new directory myplugin will be created at the same level as the empty directory (both in /path/to/glpi/plugin directory) as well as files and methods associated with an empty plugin skeleton.

Note

โ„น๏ธ If you cloned the empty project outside your GLPI instance, you can define a destination directory for your new plugin:

./plugin.sh myplugin 0.0.1 /path/to/another/glpi/plugins/

Retrieving Composer dependenciesยถ

In a terminal, run the following command:

cd /path/to/glpi/plugins/myplugin
composer install

Minimal plugin structureยถ

๐Ÿ“‚ glpi
  ๐Ÿ“‚ plugins
    ๐Ÿ“‚ myplugin
       ๐Ÿ“ ajax
       ๐Ÿ“ front
       ๐Ÿ“ src
       ๐Ÿ“ locales
       ๐Ÿ“ tools
       ๐Ÿ“ vendor
       ๐Ÿ—‹ composer.json
       ๐Ÿ—‹ hook.php
       ๐Ÿ—‹ LICENSE
       ๐Ÿ—‹ myplugin.xml
       ๐Ÿ—‹ myplugin.png
       ๐Ÿ—‹ Readme.md
       ๐Ÿ—‹ setup.php
  • ๐Ÿ“‚ front directory is used to store our object actions (create, read, update, delete).

  • ๐Ÿ“‚ ajax directory is used for ajax calls.

  • Your plugin own classes will be stored in the ๐Ÿ“‚ src directory.

  • gettext translations will be stored in the ๐Ÿ“‚ locales directory.

  • An optional ๐Ÿ“‚ templates directory would contain your plugin Twig template files.

  • ๐Ÿ“‚ tools directory provides some optional scripts from the empty plugin for development and maintenance of your plugin. It is now more common to get those scripts from ๐Ÿ“‚ vendor and ๐Ÿ“‚ node_modules directories.

  • ๐Ÿ“‚ vendor directory contains:

    • PHP libraries for your plugin,

    • helpful tools provided by empty model.

  • ๐Ÿ“‚ node_modules directory contains JavaScript libraries for your plugin.

  • ๐Ÿ—‹ composer.json files describes PHP dependencies for your project.

  • ๐Ÿ—‹ package.json file describes JavaScript dependencies for your project.

  • ๐Ÿ—‹ myplugin.xml file contains data description for publishing your plugin.

  • ๐Ÿ—‹ myplugin.png image is often included in previous XML file as a representation for GLPI plugins catalog

  • ๐Ÿ—‹ setup.php file is meant to instantiate your plugin.

  • ๐Ÿ—‹ hook.php file contains your plugin basic functions (install/uninstall, hooks, etc).

minimal setup.phpยถ

After running plugin.sh script, there must be a ๐Ÿ—‹ setup.php file in your ๐Ÿ“‚ myplugin directory.

It contains the following code:

๐Ÿ—‹ setup.php

1<?php
2
3define('PLUGIN_MYPLUGIN_VERSION', '0.0.1');

An optional constant declaration for your plugin version number used later in the plugin_version_myplugin function.

๐Ÿ—‹ setup.php

3<?php
4
5function plugin_init_myplugin() {
6   global $PLUGIN_HOOKS;
7
8   $PLUGIN_HOOKS['csrf_compliant']['myplugin'] = true;
9}

This instanciation function is important, we will declare later here Hooks on GLPI internal API. Itโ€™s systematically called on all GLPI pages except if the _check_prerequisites fails (see below). We declare here that our plugin forms are CSRF compliant even if for now our plugin does not contain any form.

๐Ÿ—‹ setup.php

 9<?php
10
11// Minimal GLPI version, inclusive
12define("PLUGIN_MYPLUGIN_MIN_GLPI_VERSION", "10.0.0");
13
14// Maximum GLPI version, exclusive
15define("PLUGIN_MYPLUGIN_MAX_GLPI_VERSION", "10.0.99");
16
17function plugin_version_myplugin()
18{
19    return [
20        'name'           => 'MonNouveauPlugin',
21        'version'        => PLUGIN_MYPLUGIN_VERSION,
22        'author'         => '<a href="http://www.teclib.com">Teclib\'</a>',
23        'license'        => 'MIT',
24        'homepage'       => '',
25        'requirements'   => [
26            'glpi' => [
27                'min' => PLUGIN_MYPLUGIN_MIN_GLPI_VERSION,
28                'max' => PLUGIN_MYPLUGIN_MAX_GLPI_VERSION,
29            ]
30    ];
31}

This function specifies data that will be displayed in the Setup > Plugins menu of GLPI as well as some minimal constraints. We reuse the constant PLUGIN_MYPLUGIN_VERSION declared above. You can of course change data according to your needs.

Note

โ„น๏ธ Choosing a license

The choice of a license is important and has many consequences on the future use of your developments. Depending on your preferences, you can choose a more permissive or restrictive orientation. Websites that can be of help exists, like https://choosealicense.com/.

In our example, MIT license has been choose. Itโ€™s a very popular choice which gives user enough liberty using your work. It just asks to keep the notice (license text) and respect the copyright. You canโ€™t be dispossessed of your work, paternity must be kept.

๐Ÿ—‹ setup.php

32<?php
33
34function plugin_myplugin_check_config($verbose = false)
35{
36    if (true) { // Your configuration check
37        return true;
38    }
39
40    if ($verbose) {
41        _e('Installed / not configured', 'myplugin');
42    }
43
44    return false;
45}

This function is systematically called on all GLPI pages. It allows to automatically deactivate plugin if defined criteria are not or no longer met (returning false).

minimal hook.phpยถ

This file must contains installation and uninstallation functions:

๐Ÿ—‹ hook.php

 1<?php
 2
 3function plugin_myplugin_install()
 4{
 5    return true;
 6}
 7
 8function plugin_myplugin_uninstall()
 9{
10    return true;
11}

When all steps are OK, we must return true. We will populate these functions later while creating/removing database tables.

Install your pluginยถ

my plugin in confiuguration

Following those first steps, you should be able to install and activate your plugin from Setup > Plugins menu.

Creating an objectยถ

๐Ÿ“ In this part, we will add an itemtype to our plugin and make it interact with GLPI.
This will be a parent object that will regroup several โ€œassetsโ€.
We will name it โ€œSuperassetโ€.

CommonDBTM usage and classes creationยถ

This super class adds the ability to manage items in the database. Your working classes (in the src directory) can inherit from it and are called โ€œitemtypeโ€ by convention.

Note

โ„น๏ธ Conventions:

  • Classes must respect PSR-12 naming conventions. We maintain a guide on coding standards

  • SQL tables related to your classes must respect that naming convention: glpi_plugin_pluginkey_names

    • a global glpi_ prefix

    • a prefix for plugins plugin_

    • plugin key myplugin_

    • itemtype name in plural form superassets

  • Tables columns must also follow some conventions:

    • there must be an auto-incremented primary field named id

    • foreign keys names use that referenced table name without the global glpi_ prefix and with and _id suffix. example: plugin_myotherclasses_id references glpi_plugin_myotherclasses table

    Warning! GLPI does not use database foreign keys constraints. Therefore you must not use FOREIGN or CONSTRAINT keys.

  • Some extra advice:

    Main reason for that is to avoid concatenation errors when using require/include functions, and prevent unexpected outputs.

We will create our first class in ๐Ÿ—‹ Superasset.php file in our plugin ๐Ÿ“‚src directory:

๐Ÿ“‚glpi
   ๐Ÿ“‚plugins
      ๐Ÿ“‚myplugin
         ...
         ๐Ÿ“‚src
            ๐Ÿ—‹ Superasset.php
         ...

We declare a few parts:

๐Ÿ—‹ src/Superasset.php

 1<?php
 2namespace GlpiPlugin\Myplugin;
 3
 4use CommonDBTM;
 5
 6class Superasset extends CommonDBTM
 7{
 8    // right management, we'll change this later
 9    static $rightname = 'computer';
10
11    /**
12     *  Name of the itemtype
13     */
14    static function getTypeName($nb=0)
15    {
16        return _n('Super-asset', 'Super-assets', $nb);
17    }
18}

Warning

โš ๏ธ namespace must be CamelCase

Note

โ„น๏ธ Here are most common CommonDBTM inherited methods:

add(array $input) : Add an new object in database table. input parameter contains table fields. If add goes well, the object will be populated with provided data. It returns the id of the new added line, or false if there were an error.

 1 <?php
 2
 3 namespace GlpiPlugin\Myplugin;
 4
 5 $superasset = new Superasset;
 6 $superassets_id = $superasset->add([
 7     'name' => 'My super asset'
 8 ]);
 9 if (!superassets_id) {
10     //super asset has not been created :'(
11 }

getFromDB(integer $id) : load an item from database into current object using its id. Fetched data will be available from fields object property. It returns false if the object does not exists.

11<?php
12
13if ($superasset->getFromDB($superassets_id)) {
14    //super $superassets_id has been lodaded.
15    //you can access its data from $superasset->fields
16}

update(array $input) : update fields of id identified line with $input parameter. The id key must be part of $input. Returns a boolean.

16<?php
17
18if (
19    $superasset->update([
20        'id'      => $superassets_id,
21        'comment' => 'my comments'
22    ])
23) {
24    //super asset comment has been updated in databse.
25}

delete(array $input, bool $force = false) : remove id identified line corresponding. The id key must be part of $input. $force parameter indicates if the line must be place in trashbin (false, and a is_deleted field must be present in the table) or removed (true). Returns a boolean.

23<?php
24
25if ($superasset->delete(['id' => $superassets_id])) {
26    //super asset has been moved to trashbin
27}
28
29if ($superasset->delete(['id' => $superassets_id], true)) {
30    //super asset is no longer present in database.
31    //a message will be displayed to user on next displayed page.
32}

Installationยถ

In the plugin_myplugin_install function of your ๐Ÿ—‹ hook.php file, we will manage the creation of the database table corresponding to our itemtype Superasset.

๐Ÿ—‹ hook.php

 1<?php
 2
 3use DBConnection;
 4use GlpiPlugin\Myplugin\Superasset;
 5use Migration;
 6
 7function plugin_myplugin_install()
 8{
 9    global $DB;
10
11    $default_charset   = DBConnection::getDefaultCharset();
12    $default_collation = DBConnection::getDefaultCollation();
13
14    // instantiate migration with version
15    $migration = new Migration(PLUGIN_MYPLUGIN_VERSION);
16
17    // create table only if it does not exist yet!
18    $table = Superasset::getTable();
19    if (!$DB->tableExists($table)) {
20        //table creation query
21        $query = "CREATE TABLE `$table` (
22                  `id`         int unsigned NOT NULL AUTO_INCREMENT,
23                  `is_deleted` TINYINT NOT NULL DEFAULT '0',
24                  `name`      VARCHAR(255) NOT NULL,
25                  PRIMARY KEY  (`id`)
26                 ) ENGINE=InnoDB
27                 DEFAULT CHARSET={$default_charset}
28                 COLLATE={$default_collation}";
29        $DB->queryOrDie($query, $DB->error());
30    }
31
32    //execute the whole migration
33    $migration->executeMigration();
34
35    return true;
36}

In addition, of a primary key, VARCHAR field to store a name entered by the user and a flag for the the trashbin.

Note

๐Ÿ“ You of course can add some other fields with other types (stay reasonable ๐Ÿ˜‰).

To handle migration from a version to another of our plugin, we will use GLPI Migration class.

๐Ÿ—‹ hook.php

 1<?php
 2
 3use Migration;
 4
 5function plugin_myplugin_install()
 6{
 7    global $DB;
 8
 9    // instantiate migration with version
10    $migration = new Migration(PLUGIN_MYPLUGIN_VERSION);
11
12    ...
13
14    if ($DB->tableExists($table)) {
15        // missing field
16        $migration->addField(
17            $table,
18            'fieldname',
19            'string'
20        );
21
22        // missing index
23        $migration->addKey(
24            $table,
25            'fieldname'
26        );
27    }
28
29    //execute the whole migration
30    $migration->executeMigration();
31
32    return true;
33}

Warning

โ„น๏ธ Migration class provides several methods that permit to manipulate tables and fields. All calls will be stored in queue that will be executed when calling executeMigration method.

Here are some examples:

addField($table, $field, $type, $options)

adds a new field to a table

changeField($table, $oldfield, $newfield, $type, $options)

change a field name or type

dropField($table, $field)

drops a field

dropTable($table)

drops a table

renameTable($oldtable, $newtable)

rename a table

See Migration documentation for all other possibilities.


$type parameter of different functions is the same as the private Migration::fieldFormat() method it allows shortcut for most common SQL types (bool, string, integer, date, datetime, text, longtext, autoincrement, char)

Uninstallationยถ

To uninstall our plugin, we want to clean all related data.

๐Ÿ—‹ hook.php

 1<?php
 2
 3use GlpiPlugin\Myplugin\Superasset;
 4
 5function plugin_myplugin_uninstall()
 6{
 7    global $DB;
 8
 9    $tables = [
10        Superasset::getTable(),
11    ];
12
13    foreach ($tables as $table) {
14        if ($DB->tableExists($table)) {
15            $DB->doQueryOrDie(
16                "DROP TABLE `$table`",
17                $DB->error()
18            );
19        }
20    }
21
22   return true;
23}

Framework usageยถ

Some useful functions

<?php

Toolbox::logError($var1, $var2, ...);

This method stored in glpi/files/_log/php-errors.log file content of its parameters (may be strings, arrays, objects, etc).

<?php

Html::printCleanArray($var);

This method will display a โ€œdebugโ€ array of the provided variable. It only accepts array type.

Common actions on an objectยถ

Note

๐Ÿ“ We will now add most common actions to our Superasset itemtype:

  • display a list and a form to add/edit

  • define add/edit/delete routes

In our front directory, we will need two new files.

๐Ÿ“‚ glpi
   ๐Ÿ“‚ plugins
      ๐Ÿ“‚ myplugin
         ...
         ๐Ÿ“‚ front
            ๐Ÿ—‹ superasset.php
            ๐Ÿ—‹ superasset.form.php
         ...

Warning

โ„น๏ธ Into those files, we will import GLPI framework with the following:

<?php

include ('../../../inc/includes.php');

First file (superasset.php) will display list of items stored in our table.

It will use the internal search engine show method of the search engine.

๐Ÿ—‹ front/superasset.php

 1<?php
 2
 3use GlpiPlugin\Myplugin\Superasset;
 4use Search;
 5use Html;
 6
 7include ('../../../inc/includes.php');
 8
 9Html::header(
10    Superasset::getTypeName(),
11    $_SERVER['PHP_SELF'],
12    "plugins",
13    Superasset::class,
14    "superasset"
15);
16Search::show(Superasset::class);
17Html::footer();

header and footer methods from Html class permit to rely on GLPI graphical user interface (menu, breadcrumb, page footer, etc).

Second file (superasset.form.php - with .form suffix) will handle CRUD actions.

๐Ÿ—‹ front/superasset.form.php

 1<?php
 2
 3use GlpiPlugin\Myplugin\Superasset;
 4use Html;
 5
 6include ('../../../inc/includes.php');
 7
 8$supperasset = new Superasset();
 9
10if (isset($_POST["add"])) {
11    $newID = $supperasset->add($_POST);
12
13    if ($_SESSION['glpibackcreated']) {
14        Html::redirect(Superasset::getFormURL()."?id=".$newID);
15    }
16    Html::back();
17
18} else if (isset($_POST["delete"])) {
19    $supperasset->delete($_POST);
20    $supperasset->redirectToList();
21
22} else if (isset($_POST["restore"])) {
23    $supperasset->restore($_POST);
24    $supperasset->redirectToList();
25
26} else if (isset($_POST["purge"])) {
27    $supperasset->delete($_POST, 1);
28    $supperasset->redirectToList();
29
30} else if (isset($_POST["update"])) {
31    $supperasset->update($_POST);
32    \Html::back();
33
34} else {
35    // fill id, if missing
36    isset($_GET['id'])
37        ? $ID = intval($_GET['id'])
38        : $ID = 0;
39
40    // display form
41    Html::header(
42       Superasset::getTypeName(),
43       $_SERVER['PHP_SELF'],
44       "plugins",
45       Superasset::class,
46       "superasset"
47    );
48    $supperasset->display(['id' => $ID]);
49    Html::footer();
50}

All common actions defined here are handled from CommonDBTM class. For missing display action, we will create a showForm method in our Superasset class. Note this one already exists in CommonDBTM and is displayed using a generic Twig template.

We will use our own template that will extends the generic one (because it only displays common fields).

๐Ÿ—‹ src/Superasset.php

 1<?php
 2
 3namespace GlpiPlugin\Myplugin;
 4
 5use CommonDBTM;
 6use Glpi\Application\View\TemplateRenderer;
 7
 8class Superasset extends CommonDBTM
 9{
10
11     ...
12
13    function showForm($ID, $options=[])
14    {
15        $this->initForm($ID, $options);
16        // @myplugin is a shortcut to the **templates** directory of your plugin
17        TemplateRenderer::getInstance()->display('@myplugin/superasset.form.html.twig', [
18            'item'   => $this,
19            'params' => $options,
20        ]);
21
22        return true;
23    }
24}

๐Ÿ—‹ templates/superasset.form.html.twig

1{% extends "generic_show_form.html.twig" %}
2{% import "components/form/fields_macros.html.twig" as fields %}
3
4{% block more_fields %}
5    blabla
6{% endblock %}

After that step, a call in our browser to http://glpi/plugins/myplugin/front/superasset.form.php should display the creation form.

Warning

โ„น๏ธ ๐Ÿ—‹ components/form/fields_macros.html.twig file imported in the example includes Twig functions or macros to display common HTML fields like:

{{ fields.textField(name, value, label = '', options = {}) }} : HTML code for a text input.

{{ fields.hiddenField(name, value, label = '', options = {}) } : HTML code for a hidden input.

{{ dateField(name, value, label = '', options = {}) } : HTML code for a date picker (using flatpickr)

{{ datetimeField(name, value, label = '', options = {}) } : HTML code for a datetime picker (using flatpickr)

See ๐Ÿ—‹ templates/components/form/fields_macros.html.twig file in source code for more details and capacities.

Adding to menu and breadcrumbยถ

We would like to access our pages without entering their URL in our browser.

Weโ€™ll therefore define our first Hook in our plugin init.

Open setup.php and edit plugin_init_myplugin function:

๐Ÿ—‹ setup.php

 1<?php
 2
 3use GlpiPlugin\Myplugin\Superasset;
 4
 5function plugin_init_myplugin()
 6{
 7    ...
 8
 9    // add menu hook
10    $PLUGIN_HOOKS['menu_toadd']['myplugin'] = [
11        // insert into 'plugin menu'
12        'plugins' => Superasset::class
13    ];
14}

This hook indicates our Superasset itemtype defines a menu display function. Edit our class and add related methods:

๐Ÿ—‹ src/Superasset.php

 1<?php
 2
 3namespace GlpiPlugin\Myplugin;
 4
 5use CommonDBTM;
 6
 7class Superasset extends CommonDBTM
 8{
 9    ...
10
11    /**
12     * Define menu name
13     */
14    static function getMenuName($nb = 0)
15    {
16        // call class label
17        return self::getTypeName($nb);
18    }
19
20    /**
21     * Define additionnal links used in breacrumbs and sub-menu
22     *
23     * A default implementation is provided by CommonDBTM
24     */
25    static function getMenuContent()
26    {
27        $title  = self::getMenuName(Session::getPluralNumber());
28        $search = self::getSearchURL(false);
29        $form   = self::getFormURL(false);
30
31        // define base menu
32        $menu = [
33            'title' => __("My plugin", 'myplugin'),
34            'page'  => $search,
35
36            // define sub-options
37            // we may have multiple pages under the "Plugin > My type" menu
38            'options' => [
39                'superasset' => [
40                    'title' => $title,
41                    'page'  => $search,
42
43                    //define standard icons in sub-menu
44                    'links' => [
45                        'search' => $search,
46                        'add'    => $form
47                    ]
48                ]
49            ]
50        ];
51
52        return $menu;
53    }
54}

getMenuContent function may seem redundant at first, but each of the coded entries relates to different parts of the display. The options part is used to have a fourth level of breadcrumb and thus have a clickable submenu in your entry page.

Breadcrumb

Each page key is used to indicate on which URL the current part applies.

Note

โ„น๏ธ GLPI menu is loaded in $_SESSION['glpimenu'] on login. To see your changes, either use the DEBUG mode, or disconnect and reconnect.

Note

โ„น๏ธ It is possible to have only one menu level for the plugin (3 globally), just move the links part to the first level of the $menu array.

Note

โ„น๏ธ It is also possible to define custom links. You just need to replace the key (for example, add or search) with an html containing an image tag:

'links' = [
    '<img src="path/to/my.png" title="my custom link">' => $url
]

Defining tabsยถ

GLPI proposes three methods to define tabs:

defineTabs(array $options = []): declares classes that provides tabs to current class.

getTabNameForItem(CommonGLPI $item, boolean $withtemplate = 0): declares titles displayed for tabs.

displayTabContentForItem(CommonGLPI $item, integer $tabnum = 1, boolean $withtemplate = 0): allow displaying tabs contents.

Standards tabsยถ

Some GLPI internal API classes allows you to add a behaviour with minimal code.

Itโ€™s true for notes (Notepad) and history (Log).

Here is an example for both of them:

๐Ÿ—‹ src/Superasset.php

 1<?php
 2
 3namespace GlpiPlugin\Myplugin;
 4
 5use CommonDBTM;
 6use Notepad;
 7use Log;
 8
 9class Superasset extends CommonDBTM
10{
11    // permits to automaticaly store logs for this itemtype
12    // in glpi_logs table
13    public $dohistory = true;
14
15    ...
16
17    function defineTabs($options = [])
18    {
19        $tabs = [];
20        $this->addDefaultFormTab($tabs)
21            ->addStandardTab(Notepad::class, $tabs, $options)
22            ->addStandardTab(Log::class, $tabs, $options);
23
24        return $tabs;
25    }
26}

Display of an instance of your itemtype from the page front/superasset.php?id=1 should now have 3 tabs:

  • Main tab with your itemtype name

  • Notes tab

  • History tab

Custom tabsยถ

On a similar basis, we can target another class of our plugin:

๐Ÿ—‹ src/Superasset.php

 1<?php
 2
 3namespace GlpiPlugin\Myplugin;
 4
 5use CommonDBTM;
 6use Notepad;
 7use Log;
 8
 9class Superasset extends CommonDBTM
10{
11    // permits to automaticaly store logs for this itemtype
12    // in glpi_logs table
13    public $dohistory = true;
14
15    ...
16
17    function defineTabs($options = [])
18    {
19        $tabs = [];
20        $this->addDefaultFormTab($tabs)
21            ->addStandardTab(Superasset_Item::class, $tabs, $options);
22            ->addStandardTab(Notepad::class, $tabs, $options)
23            ->addStandardTab(Log::class, $tabs, $options);
24
25        return $tabs;
26    }

In this new class we will define two other methods to control title and content of the tab:

๐Ÿ—‹ src/Superasset_Item.php

 1<?php
 2
 3namespace GlpiPlugin\Myplugin;
 4
 5use CommonDBTM;
 6use Glpi\Application\View\TemplateRenderer;
 7
 8class Superasset_Item extends CommonDBTM
 9{
10    /**
11     * Tabs title
12     */
13    function getTabNameForItem(CommonGLPI $item, $withtemplate = 0)
14    {
15        switch ($item->getType()) {
16            case Superasset::class:
17                $nb = countElementsInTable(self::getTable(),
18                    [
19                        'plugin_myplugin_superassets_id' => $item->getID()
20                    ]
21                );
22                return self::createTabEntry(self::getTypeName($nb), $nb);
23        }
24        return '';
25    }
26
27    /**
28     * Display tabs content
29     */
30    static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0)
31    {
32        switch ($item->getType()) {
33            case Superasset::class:
34                return self::showForSuperasset($item, $withtemplate);
35        }
36
37        return true;
38    }
39
40    /**
41     * Specific function for display only items of Superasset
42     */
43    static function showForSuperasset(Superasset $superasset, $withtemplate = 0)
44    {
45        TemplateRenderer::getInstance()->display('@myplugin/superasset_item_.html.twig', [
46            'superasset' => $superasset,
47        ]);
48    }
49}

As previously, we will use a Twig template to handle display.

๐Ÿ—‹ templates/superasset_item.html.twig

1{% import "components/form/fields_macros.html.twig" as fields %}
2
3example content

Note

๐Ÿ“ Exercise: For the rest of this part, you will need to complete our plugin to allow the installation/uninstallation of the data of this new class Superasset_Item.

Table should contains following fields:

  • an identifier (id)

  • a foreign key to plugin_myplugin_superassets table

  • two fields to link with an itemtype:

    • itemtype which will store the itemtype class to link to (Computer for example)

    • items_id the id of the linked asset

Your plugin must be re-installed or updated for the table creation to be done. You can force the plugin status to change by incrementing the version number in the setup.php file.

For the exercise, we will only display computers (Computer) displayed with the following code:

{{ fields.dropdownField(
    'Computer',
    'items_id',
    '',
    __('Add a computer')
) }}

We will include a mini form to insert related items in our table. Form actions can be handled from myplugin/front/supperasset.form.php file.

Note GLPI forms submitted as POST will be protected with a CRSF token.. You can include a hidden field to validate the form:

<input type="hidden" name="_glpi_csrf_token" value="{{ csrf_token() }}">

We will also display a list of computers already associated below the form.

Using core objectsยถ

We can also allow our class to add tabs on core objects. We will declare this in a new line in our init function:

๐Ÿ—‹ setup.php

 1<?php
 2
 3use Computer;
 4
 5function plugin_init_myplugin()
 6{
 7   ...
 8
 9    Plugin::registerClass(GlpiPlugin\Myplugin\Superasset_Item::class, [
10        'addtabon' => Computer::class
11    ]);
12}

Title and content for this tab are done as previously with:

  • CommonDBTM::getTabNameForItem()

  • CommonDBTM::displayTabContentForItem()

Note

๐Ÿ“ Exercise: Complete previous methods to display on computers a new tab with associated Superasset.

Defining Search optionsยถ

Search options is an array of columns for GLPI search engine. They are used to know for each itemtype how the database must be queried, and how data should be displayed.

In our class, we must declare a rawSearchOptions method:

๐Ÿ—‹ src/Superasset.php

 1<?php
 2
 3namespace GlpiPlugin\Myplugin;
 4
 5use CommonDBTM;
 6
 7class Superasset extends CommonDBTM
 8{
 9    ...
10
11    function rawSearchOptions()
12    {
13        $options = [];
14
15        $options[] = [
16            'id'   => 'common',
17            'name' => __('Characteristics')
18        ];
19
20        $options[] = [
21            'id'    => 1,
22            'table' => self::getTable(),
23            'field' => 'name',
24            'name'  => __('Name'),
25            'datatype' => 'itemlink'
26        ];
27
28        $options[] = [
29            'id'    => 2,
30            'table' => self::getTable(),
31            'field' => 'id',
32            'name'  => __('ID')
33        ];
34
35        $options[] = [
36            'id'           => 3,
37            'table'        => Superasset_Item::getTable(),
38            'field'        => 'id',
39            'name'         => __('Number of associated assets', 'myplugin'),
40            'datatype'     => 'count',
41            'forcegroupby' => true,
42            'usehaving'    => true,
43            'joinparams'   => [
44                'jointype' => 'child',
45            ]
46        ];
47
48        return $options;
49    }
50}

Following this addition, we should be able to select our new columns from our asset list page:

Search form

Those options will also be present in search criteria list of that page.

Each option is identified by an id key. This key is used in other parts of GLPI. It must be absolutely unique. By convention, โ€˜1โ€™ and โ€˜2โ€™ are โ€œreservedโ€ for the object name and ID.

The search options documentation describes all possible options.

Using other objectsยถ

It is also possible to improve another itemtype search options. As an example, we would like to display associated โ€œSuperassetโ€ on in the computer list:

๐Ÿ—‹ hook.php

50<?php
51
52use GlpiPlugin\Myplugin\Superasset;
53use GlpiPlugin\Myplugin\Superasset_Item;
54
55...
56
57function plugin_myplugin_getAddSearchOptionsNew($itemtype)
58{
59    $sopt = [];
60
61    if ($itemtype == 'Computer') {
62        $sopt[] = [
63            'id'           => 12345,
64            'table'        => Superasset::getTable(),
65            'field'        => 'name',
66            'name'         => __('Associated Superassets', 'myplugin'),
67            'datatype'     => 'itemlink',
68            'forcegroupby' => true,
69            'usehaving'    => true,
70            'joinparams'   => [
71                'beforejoin' => [
72                    'table'      => Superasset_Item::getTable(),
73                    'joinparams' => [
74                        'jointype' => 'itemtype_item',
75                    ]
76                ]
77            ]
78        ];
79    }
80
81    return $sopt;
82}

As previously, you must provide an id for your new search options that does not override existing ones for Computer.

You can use a script from the tools folder of the GLPI git repository (not present in the โ€œreleaseโ€ archives) to help you list the id already declared (by the core and plugins present on your computer) for a particular itemtype.

/usr/bin/php /path/to/glpi/tools/getsearchoptions.php --type=Computer

Search engine display preferencesยถ

We just have added new columns to our itemtype list. Those columns are handled by DisplayPreference object (glpi_displaypreferences table). They can be defined as global (set 0 for users_id field) or personal (set users_id field to the user id). They are sorted (rank field) and target an itemtype plus a searchoption (num field).

Warning

โš ๏ธ Warning Global preferences are applied to all users that donโ€™t have any personal preferences set.

Note

๐Ÿ“ Exercise: You will change installation and uninstallation functions of your plugin to add and remove global preferences so objects list display some columns.

Standard events hooksยถ

During a GLPI object life cycle, we can intervene via our plugin before and after each event (add, modify, delete).

For our own objects, following methods can be implemented:

For every event applied on the database, we have a method that is executed before, and another after.

Note

๐Ÿ“ Exercise: Add required methods to PluginMypluginSuperasset class to check the name field is properly filled when adding and updating.

On effective removal, we must ensure linked data from other tables are also removed.

Plugins can also intercept standard core events to apply changes (or even refuse the event). Here are the names of the hooks:

 1<?php
 2
 3use Glpi\Plugin\Hooks;
 4
 5...
 6
 7Hooks::PRE_ITEM_ADD;
 8Hooks::ITEM_ADD;
 9Hooks::PRE_ITEM_DELETE;
10Hooks::ITEM_DELETE;
11Hooks::PRE_ITEM_PURGE;
12Hooks::ITEM_PURGE;
13Hooks::PRE_ITEM_RESTORE;
14Hooks::ITEM_RESTORE;
15Hooks::PRE_ITEM_UPDATE;
16Hooks::ITEM_UPDATE;

More information are available from hooks documentation especially on standard events part.

For all those calls, we will get an instance of the current object in parameter of our callback function. We will be able to access its current fields ($item->fields) or those sent by the form ($item->input). As all PHP objects, this instance will be passed by reference.

We will declare one of those hooks usage in the plugin init function and add a callback function:

๐Ÿ—‹ setup.php

 1<?php
 2
 3use GlpiPlugin\Myplugin\Superasset;
 4
 5...
 6
 7function plugin_init_myplugin()
 8{
 9   ...
10
11    // callback a function (declared in hook.php)
12    $PLUGIN_HOOKS['item_update']['myplugin'] = [
13        'Computer' => 'myplugin_computer_updated'
14    ];
15
16    // callback a class method
17    $PLUGIN_HOOKS['item_add']['myplugin'] = [
18         'Computer' => [
19              Superasset::class, 'computerUpdated'
20         ]
21    ];
22}

In both cases (hook.php function or class method), the prototype of the functions will be made on this model:

 1<?php
 2
 3use CommonDBTM;
 4use Session;
 5
 6function hookCallback(CommonDBTM $item)
 7{
 8    ...
 9
10    // if we need to stop the process (valid for pre* hooks)
11    if ($mycondition) {
12        // clean input
13        $item->input = [];
14
15        // store a message in session for warn user
16        Session::addMessageAfterRedirect('Action forbidden because...');
17
18        return;
19   }
20}

Note

๐Ÿ“ Exercise: Use a hook to intercept the purge of a computer and remove associated with a Superasset lines if any.

Importing libraries (JavaScript / CSS)ยถ

Plugins can declare import of additional libraries from their init function.

๐Ÿ—‹ setup.php

 1<?php
 2
 3use Glpi\Plugin\Hooks;
 4
 5function plugin_init_myplugin()
 6{
 7    ...
 8
 9    // css & js
10    $PLUGIN_HOOKS[Hooks::ADD_CSS]['myplugin'] = 'myplugin.css';
11    $PLUGIN_HOOKS[Hooks::ADD_JAVASCRIPT]['myplugin'] = [
12        'js/common.js',
13    ];
14
15    // on ticket page (in edition)
16    if (strpos($_SERVER['REQUEST_URI'], "ticket.form.php") !== false
17        && isset($_GET['id'])) {
18        $PLUGIN_HOOKS['add_javascript']['myplugin'][] = 'js/ticket.js.php';
19    }
20
21    ...
22}

Several things to remember:

  • Loading paths are relative to plugin directory.

  • Scripts declared this way will be loaded on all GLPI pages. You must check the current page in the init function.

  • You can rely on Html::requireJs() method to load external resources. Paths will be prefixed with GLPI root URL at load.

  • If you want to modify page DOM and especially what is displayed in main form, you should call your code twice (on page load and on current tab load) and add a class to check the effective application of your code:

 1$(function() {
 2    doStuff();
 3    $(".glpi_tabs").on("tabsload", function(event, ui) {
 4        doStuff();
 5    });
 6});
 7
 8var doStuff = function()
 9{
10    if (! $("html").hasClass("stuff-added")) {
11        $("html").addClass("stuff-added");
12
13        // do stuff you need
14        ...
15
16    }
17};

Note

๐Ÿ“ Exercises:

  1. Add a new icon in preferences menu to display main GLPI configuration. You can use tabler-icons:

  • <a href='...' class='ti ti-mood-smile'></a>

  1. On ticket edition page, add an icon to self-associate as a requester on the model of the one present for the โ€œassigned toโ€ part.

Display hooksยถ

Since GLPI 9.1.2, it is possible to display data in native objects forms via new hooks. See display related hooks in plugins documentation.

As previous hooks, declaration will look like:

๐Ÿ—‹ setup.php

 1<?php
 2
 3use Glpi\Plugin\Hooks;
 4use GlpiPlugin\Myplugin\Superasset;
 5
 6function plugin_init_myplugin()
 7{
 8   ...
 9
10    $PLUGIN_HOOKS[Hooks::PRE_ITEM_FORM]['myplugin'] = [
11        Superasset::class, 'preItemFormComputer'
12    ];
13}

Warning

โ„น๏ธ Important Those display hooks are a bit different from other hooks regarding parameters that are passed to callback underlying method. We will obtain an array with the following keys:

  • item with current CommonDBTM object

  • options, an array passed from current object showForm() method

example of a call from core:

<?php

Plugin::doHook("pre_item_form", ['item' => $this, 'options' => &$options]);

Note

๐Ÿ“ Exercice: Add the number of associated Superasset in the computer form header. It should be a link to the previous added tab to computers. This link will target the same page, but with the forcetab=PluginMypluginSuperasset$1 parameter.

Adding a configuration pageยถ

We will add a tab in GLPI configuration so some parts of our plugin can be optional.

We previously added a tab to the form for computers using hooks in setup.php file. We will define two configuration options to enable/disable those tabs.

GLPI provides a glpi_configs table to store software configuration. It allows plugins to save their own data without defining additional tables.

First of all, letโ€™s create a new Config.php class in the src/ folder with the following skeleton:

๐Ÿ—‹ src/Config.php

 1<?php
 2
 3namespace GlpiPlugin\Myplugin;
 4
 5use Config;
 6use CommonGLPI;
 7use Dropdown;
 8use Html;
 9use Session;
10use Glpi\Application\View\TemplateRenderer;
11
12class Config extends \Config
13{
14
15    static function getTypeName($nb = 0)
16    {
17        return __('My plugin', 'myplugin');
18    }
19
20    static function getConfig()
21    {
22        return Config::getConfigurationValues('plugin:myplugin');
23    }
24
25    function getTabNameForItem(CommonGLPI $item, $withtemplate = 0)
26    {
27        switch ($item->getType()) {
28            case Config::class:
29                return self::createTabEntry(self::getTypeName());
30        }
31        return '';
32    }
33
34    static function displayTabContentForItem(
35        CommonGLPI $item,
36        $tabnum = 1,
37        $withtemplate = 0
38    ) {
39        switch ($item->getType()) {
40            case Config::class:
41                return self::showForConfig($item, $withtemplate);
42        }
43
44        return true;
45    }
46
47    static function showForConfig(
48        Config $config,
49        $withtemplate = 0
50    ) {
51        global $CFG_GLPI;
52
53        if (!self::canView()) {
54            return false;
55        }
56
57        $current_config = self::getConfig();
58        $canedit        = Session::haveRight(self::$rightname, UPDATE);
59
60        TemplateRenderer::getInstance()->display('@myplugin/config.html.twig', [
61            'current_config' => $current_config,
62            'can_edit'       => $canedit
63        ]);
64    }
65}

Once again, we manage display from a dedicated template file:

๐Ÿ—‹ templates/config.html.twig

 1{% import "components/form/fields_macros.html.twig" as fields %}
 2
 3{% if can_edit %}
 4    <form name="form" action="{{ "Config"|itemtype_form_path }}" method="POST">
 5        <input type="hidden" name="config_class" value="GlpiPlugin\\Myplugin\\Config">
 6        <input type="hidden" name="config_context" value="plugin:myplugin">
 7        <input type="hidden" name="_glpi_csrf_token" value="{{ csrf_token() }}">
 8
 9        {{ fields.dropdownYesNo(
10            'myplugin_computer_tab',
11            current_config['myplugin_computer_tab'],
12            __("Display tab in computer", 'myplugin')
13        ) }}
14
15        {{ fields.dropdownYesNo(
16            'myplugin_computer_form',
17            current_config['myplugin_computer_form'],
18            __("Display information in computer form", 'myplugin')
19        ) }}
20
21        <button type="submit" class="btn btn-primary mx-1" name="update" value="1">
22            <i class="ti ti-device-floppy"></i>
23            <span>{{ _x('button', 'Save') }}</span>
24        </button>
25    </form>
26{% endif %}

This skeleton retrieves the calls to a tab in the Setup > General menu to display the dedicated form. It is useless to add a front file because the GLPI Config object already offers a form display.

Note that we display, from the myplugin_computer_form two yes/no fields named myplugin_computer_tab and myplugin_computer_form.

Note

โœ๏ธ Complete setup.php file by defining the new tab in the Config class.

You also have to add those new configuration entries management to install/uninstall methods. You can use the following:

<?php

use Config;

Config::setConfigurationValues('##context##', [
    '##config_name##' => '##config_default_value##'
]);
<?php

use Config;

$config = new Config();
$config->deleteByCriteria(['context' => '##context##']);

Do not forget to replace ## surrounded terms with your own values!

Managing rightsยถ

To limit access to our plugin features to some of our users, we can use the GLPI Profile class.

This will check $rightname property of class that inherits CommonDBTM for all standard events. Those check are done by static can* functions:

In order to customize rights, we will redefine those static methods in our classes.

If we need to check a right manually in our code, the Session class provides some methods:

 1<?php
 2
 3use Session;
 4
 5if (Session::haveRight(self::$rightname, CREATE)) {
 6   // OK
 7}
 8
 9// we can also test a set multiple rights with AND operator
10if (Session::haveRightsAnd(self::$rightname, [CREATE, READ])) {
11   // OK
12}
13
14// also with OR operator
15if (Session::haveRightsOr(self::$rightname, [CREATE, READ])) {
16   // OK
17}
18
19// check a specific right (not your class one)
20if (Session::haveRight('ticket', CREATE)) {
21   // OK
22}

Above methods return a boolean. If we need to stop the page with a message to the user, we can use equivalent methods with check instead of have prefix:

Warning

โ„น๏ธ If you need to check a right in an SQL query, use bitwise operators & and |:

<?php

$iterator = $DB->request([
    'SELECT' => 'glpi_profiles_users.users_id',
    'FROM' => 'glpi_profiles_users',
    'INNER JOIN' => [
        'glpi_profiles' => [
            'ON' => [
                'glpi_profiles_users' => 'profiles_id'
                 'glpi_profiles' => 'id'
            ]
        ],
        'glpi_profilerights' => [
            'ON' => [
                'glpi_profilerights' => 'profiles_id',
                 'glpi_profiles' => 'id'
            ]
        ]
    ],
    'WHERE' => [
        'glpi_profilerights.name' => 'ticket',
        'glpi_profilerights.rights' => ['&', (READ | CREATE)];
    ]
]);

In this code example, the READ | CREATE make a bit sum, and the & operator compare the value at logical level with the table.

Possible values for standard rights can be found in the inc/define.php file of GLPI:

 1<?php
 2
 3...
 4
 5define("READ", 1);
 6define("UPDATE", 2);
 7define("CREATE", 4);
 8define("DELETE", 8);
 9define("PURGE", 16);
10define("ALLSTANDARDRIGHT", 31);
11define("READNOTE", 32);
12define("UPDATENOTE", 64);
13define("UNLOCK", 128);

Add a new rightยถ

Note

โœ๏ธ We previously defined a property $rightname = 'computer' on which weโ€™ve automatically rights as super-admin. We will now create a specific right for the plugin.

First of all, letโ€™s create a new class dedicated to profiles management:

๐Ÿ—‹ src/Profile.php

 1<?php
 2namespace GlpiPlugin\Myplugin;
 3
 4use CommonDBTM;
 5use CommonGLPI;
 6use Html;
 7use Profile as Glpi_Profile;
 8
 9class Profile extends CommonDBTM
10{
11    public static $rightname = 'profile';
12
13    static function getTypeName($nb = 0)
14    {
15        return __("My plugin", 'myplugin');
16    }
17
18    public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0)
19    {
20        if (
21            $item instanceof Glpi_Profile
22            && $item->getField('id')
23        ) {
24            return self::createTabEntry(self::getTypeName());
25        }
26        return '';
27    }
28
29    static function displayTabContentForItem(
30        CommonGLPI $item,
31        $tabnum = 1,
32        $withtemplate = 0
33    ) {
34        if (
35            $item instanceof Glpi_Profile
36            && $item->getField('id')
37        ) {
38            return self::showForProfile($item->getID());
39        }
40
41        return true;
42    }
43
44    static function getAllRights($all = false)
45    {
46        $rights = [
47            [
48                'itemtype' => Superasset::class,
49                'label'    => Superasset::getTypeName(),
50                'field'    => 'myplugin::superasset'
51            ]
52        ];
53
54        return $rights;
55    }
56
57
58    static function showForProfile($profiles_id = 0)
59    {
60        $profile = new Glpi_Profile();
61        $profile->getFromDB($profiles_id);
62
63        TemplateRenderer::getInstance()->display('@myplugin/profile.html.twig', [
64            'can_edit' => self::canUpdate(),
65            'profile'  => $profile
66            'rights'   => self::getAllRights()
67        ]);
68    }
69}

Once again, display will be done from a Twig template:

๐Ÿ—‹ templates/profile.html.twig

 1{% import "components/form/fields_macros.html.twig" as fields %}
 2<div class='firstbloc'>
 3    <form name="form" action="{{ "Profile"|itemtype_form_path }}" method="POST">
 4        <input type="hidden" name="id" value="{{ profile.fields['id'] }}">
 5        <input type="hidden" name="_glpi_csrf_token" value="{{ csrf_token() }}">
 6
 7        {% if can_edit %}
 8            <button type="submit" class="btn btn-primary mx-1" name="update" value="1">
 9                <i class="ti ti-device-floppy"></i>
10                <span>{{ _x('button', 'Save') }}</span>
11            </button>
12        {% endif %}
13    </form>
14</div>

We declare a new tab on Profile object from our init function:

๐Ÿ—‹ setup.php

 1<?php
 2
 3use Plugin;
 4use Profile;
 5use GlpiPlugin\Myplugin\Profile as MyPlugin_Profile;
 6
 7function plugin_init_myplugin()
 8{
 9   ...
10
11    Plugin::registerClass(MyPlugin_Profile::class, [
12        'addtabon' => Profile::class
13    ]);
14}

And we tell installer to setup a minimal right for current profile (super-admin):

๐Ÿ—‹ hook.php

 1<?php
 2
 3use GlpiPlugin\Myplugin\Profile as MyPlugin_Profile;
 4use ProfileRight;
 5
 6function plugin_myplugin_install() {
 7   ...
 8
 9   // add rights to current profile
10   foreach (MyPlugin_Profile::getAllRights() as $right) {
11      ProfileRight::addProfileRights([$right['field']]);
12   }
13
14   return true;
15}
16
17function plugin_myplugin_uninstall() {
18   ...
19
20   // delete rights for current profile
21   foreach (MyPlugin_Profile::getAllRights() as $right) {
22      ProfileRight::deleteProfileRights([$right['field']]);
23   }
24
25}

Then, wa can define rights from Administration > Profiles menu and can change the $rightname property of our class to myplugin::superasset.

Extending standard rightsยถ

If we need specific rights for our plugin, for example the right to perform associations, we must override the getRights function in the class defining the rights.

In defined above example of the PluginMypluginProfile class, we added a getAllRights method which indicates that the right myplugin::superasset is defined in the PluginMypluginSuperasset class. This one inherits from CommonDBTM and has a getRights method that we can override:

๐Ÿ—‹ src/Superasset.php

 1<?php
 2
 3namespace GlpiPlugin\Myplugin;
 4
 5use CommonDBTM;
 6...
 7
 8class Superasset extends CommonDBTM
 9{
10    const RIGHT_ONE = 128;
11
12    ...
13
14    function getRights($interface = 'central')
15    {
16        // if we need to keep standard rights
17        $rights = parent::getRights();
18
19        // define an additional right
20        $rights[self::RIGHT_ONE] = __("My specific rights", "myplugin");
21
22        return $rights;
23    }
24}

Massive actionsยถ

GLPI massive actions allow applying modifications to a selection.

massive actions control

By default, GLPI proposes following actions:

  • Edit: to edit fields that are defined in search options (excepted those where massiveaction is set to false)

  • Put in trashbin/Delete

It is possible to declare extra massive actions.

To achieve that in your plugin, you must declare a hook in the init function:

๐Ÿ—‹ setup.php

1<?php
2
3function plugin_init_myplugin()
4{
5    ...
6
7    $PLUGIN_HOOKS['use_massive_action']['myplugin'] = true;
8}

Then, in the Superasset class, you must add 3 methods:

  • getSpecificMassiveActions: massive actions declaration.

  • showMassiveActionsSubForm: sub-form display.

  • processMassiveActionsForOneItemtype: handle form submit.

Here is a minimal implementation example:

๐Ÿ—‹ src/Superasset.php

 1<?php
 2
 3namespace GlpiPlugin\Myplugin;
 4
 5use CommonDBTM;
 6use Html;
 7use MassiveAction;
 8
 9class Superasset extends CommonDBTM
10{
11    ...
12
13    function getSpecificMassiveActions($checkitem = NULL)
14    {
15        $actions = parent::getSpecificMassiveActions($checkitem);
16
17        // add a single massive action
18        $class        = __CLASS__;
19        $action_key   = "myaction_key";
20        $action_label = "My new massive action";
21        $actions[$class . MassiveAction::CLASS_ACTION_SEPARATOR . $action_key] = $action_label;
22
23        return $actions;
24    }
25
26    static function showMassiveActionsSubForm(MassiveAction $ma)
27    {
28        switch ($ma->getAction()) {
29            case 'myaction_key':
30                echo __("fill the input");
31                echo Html::input('myinput');
32                echo Html::submit(__('Do it'), ['name' => 'massiveaction']) . "</span>";
33
34                break;
35        }
36
37        return parent::showMassiveActionsSubForm($ma);
38    }
39
40    static function processMassiveActionsForOneItemtype(
41        MassiveAction $ma,
42        CommonDBTM $item,
43        array $ids
44    ) {
45        switch ($ma->getAction()) {
46            case 'myaction_key':
47                $input = $ma->getInput();
48
49                foreach ($ids as $id) {
50
51                    if (
52                        $item->getFromDB($id)
53                        && $item->doIt($input)
54                    ) {
55                        $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_OK);
56                    } else {
57                        $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_KO);
58                        $ma->addMessage(__("Something went wrong"));
59                    }
60                }
61                return;
62        }
63
64        parent::processMassiveActionsForOneItemtype($ma, $item, $ids);
65    }
66}

Note

๐Ÿ“ Exercise: With the help of the official documentation on massive actions, complete in your plugin the above methods to allow the linking with a computer from โ€œSuper assetsโ€ massive actions.

You can display a list of computers with:

Computer::dropdown();

It is also possible to add massive actions to GLPI native objects. To achieve that, you must declare a _MassiveActions function in the hook.php file:

๐Ÿ—‹ hook.php

 1<?php
 2
 3use Computer;
 4use MassiveAction;
 5use GlpiPlugin\Myplugin\Superasset;
 6
 7...
 8
 9function plugin_myplugin_MassiveActions($type)
10{
11   $actions = [];
12   switch ($type) {
13      case Computer::class:
14         $class = Superasset::class;
15         $key   = 'DoIt';
16         $label = __("plugin_example_DoIt", 'example');
17         $actions[$class.MassiveAction::CLASS_ACTION_SEPARATOR.$key]
18            = $label;
19
20         break;
21   }
22   return $actions;
23}

Sub form display and processing are done the same way as you did for your plugin itemtypes.

Note

๐Ÿ“ Exercise: As the previous exercise, add a massive action to link a computer to a โ€œSuper assetโ€ from the computer list.

Do not forget to use unique keys and labels.

Notificationsยถ

Warning

โš ๏ธ Access to an SMTP server is recommended; it must be properly configured in Setup > Notifications menu. On a development environment, you can install mailhog or mailcatcher which expose a local SMTP server and allow you to get emails sent by GLPI in a graphical interface.

Please also note that GLPI queues all notifications rather than sending them directly. The only exception to this is the test email notification. All โ€œpendingโ€ notifications are visible in the Administration > Notification queue menu. You can send notifications immediately from this menu or by forcing the queuednotification automatic action.

The GLPI notification system allows sending alerts to the actors of a recorded event. By default, notifications can be sent by email or as browser notifications, but other channels may be available from plugins (or you can add your own one).

That system is divided in several classes:

  • Notification: the triggering item. It receives common data like name, if it is active, sending mode, event, content (NotificationTemplate), etc.

    Add notification form
  • NotificationTarget: defines notification recipients.

    It is possible to define recipients based on the triggering item (author, assignee) or static recipients (a specific user, all users of a specific group, etc).

    Choose actor form
  • NotificationTemplate: notification templates are used to build the content, which can be chosen from Notification form. CSS can be defined in the templates and it receives one or more NotificationTemplateTranslation instances.

    Notification template form
  • NotificationTemplateTranslation: defines the translated template content. If no language is specified, it will be the default translation. If no template translation exists for a userโ€™s language, the default translation will be used.

    The content is dynamically generated with tags provided to the user and completed by HTML.

    Template translation form

All of these notification-related object are natively managed by GLPI core and does not require any development intervention from us.

We can however trigger a notification execution via the following code:

<?php

use NotificationEvent;

NotificationEvent::raiseEvent($event, $item);

The event key corresponds to the triggering event name defined in the Notification object and the item key to the triggering item. Therefore, the raiseEvent method will search the glpi_notifications table for an active line with these 2 characteristics.

To use this trigger in our plugin, we will add a new class PluginMypluginNotificationTargetSuperasset. This ones targets our Superasset object. It is the standard way to develop notifications in GLPI. We have an itemtype with its own life and a notification object related to it.

๐Ÿ—‹ src/NotificationTargetSuperasset.php

 1<?php
 2
 3namespace GlpiPlugin\Myplugin;
 4
 5use NotificationTarget;
 6
 7class NotificationTargetSuperasset extends NotificationTarget
 8{
 9
10    function getEvents()
11    {
12        return [
13            'my_event_key' => __('My event label', 'myplugin')
14        ];
15    }
16
17    function getDatasForTemplate($event, $options = [])
18    {
19        $this->datas['##myplugin.name##'] = __('Name');
20    }
21}

We have to declare our Superasset object can send notifications in our init function:

๐Ÿ—‹ setup.php

 1<?php
 2
 3use Plugin;
 4use GlpiPlugin\Myplugin\Superasset;
 5
 6function plugin_init_myplugin()
 7{
 8   ...
 9
10    Plugin::registerClass(Superasset::class, [
11        'notificationtemplates_types' => true
12    ]);
13}

With this minimal code itโ€™s possible to create using the GLPI UI, a new notification targeting our Superasset itemtype and with the โ€˜My event labelโ€™ event and then use the raiseEvent method with these parameters.

Note

๐Ÿ“ Exercise: Along with an effective sending test, you will manage installation and uninstallation of notification and related objects (templates, translations).

You can see an example (still incomplete) on notifications in plugins documentation.

Automatic actionsยถ

This GLPI feature provides a task scheduler executed silently from user usage (GLPI mode) or by the server in command line (CLI mode) via a call to the front/cron.php file of GLPI.

To add one or more automatic actions to our class, we will add those methods:

  • cronInfo: possible actions for the class, and associated labels

  • cron*Action*: a method for each action defined in cronInfo. Those are called to manage each action.

๐Ÿ—‹ src/Superasset.php

 1<?php
 2
 3namespace GlpiPlugin\Myplugin;
 4
 5use CommonDBTM;
 6
 7class Superasset extends CommonDBTM
 8{
 9    ...
10
11    static function cronInfo($name)
12    {
13
14        switch ($name) {
15            case 'myaction':
16                return ['description' => __('action desc', 'myplugin')];
17        }
18        return [];
19    }
20
21    static function cronMyaction($task = NULL)
22    {
23        // do the action
24
25        return true;
26    }
27}

To tell GLPI that the automatic action exists, you just have to register it:

๐Ÿ—‹ hook.php

 1<?php
 2
 3use CronTask;
 4
 5function plugin_myplugin_install()
 6{
 7
 8    ...
 9
10    CronTask::register(
11        PluginMypluginSuperasset::class,
12        'myaction',
13        HOUR_TIMESTAMP,
14        [
15            'comment'   => '',
16            'mode'      => \CronTask::MODE_EXTERNAL
17        ]
18    );
19}

No need to manage uninstallation (unregister) as GLPI will handle that itself when the plugin is uninstalled.

Publishing your pluginยถ

When you consider your plugin is ready and covers a real need, you can submit it to the community.

The plugins catalog allows GLPI users to discover, download and follow plugins provided by the community as well as first-party plugins provided by Teclibโ€™.

Just publish your code to an publicly accessible GIT repository (github, gitlab, โ€ฆ) with an open source license of your choice and prepare an XML description file of your plugin. The XML file must follow this structure:

 1<root>
 2   <name>Displayed name</name>
 3   <key>System name</key>
 4   <state>stable</state>
 5   <logo>http://link/to/logo/with/dimensions/40px/40px</logo>
 6   <description>
 7      <short>
 8         <en>short description of the plugin, displayed on list, text only</en>
 9         <lang>...</lang>
10      </short>
11      <long>
12         <en>short description of the plugin, displayed on detail, Markdown accepted</en>
13         <lang>...</lang>
14      </long>
15   </description>
16   <homepage>http://link/to/your/page</homepage>
17   <download>http://link/to/your/files</download>
18   <issues>http://link/to/your/issues</issues>
19   <readme>http://link/to/your/readme</readme>
20   <authors>
21      <author>Your name</author>
22   </authors>
23   <versions>
24      <version>
25         <num>1.0</num>
26         <compatibility>10.0</compatibility>
27         <download_url>http://link/to/your/download/glpi-myplugin-1.0.tar.bz2</download_url>
28      </version>
29   </versions>
30   <langs>
31      <lang>en_GB</lang>
32      <lang>...</lang>
33   </langs>
34   <license>GPL v2+</license>
35   <tags>
36      <en>
37         <tag>tag1</tag>
38      </en>
39      <lang>
40         <tag>tag1</tag>
41      </lang>
42   </tags>
43   <screenshots>
44      <screenshot>http://link/to/your/screenshot</screenshot>
45      <screenshot>http://link/to/your/screenshot</screenshot>
46      <screenshot>...</screenshot>
47   </screenshots>
48</root>

To market this plugin to a wide range of users, you should add a detailed description in several languages and provide screenshots that represent your plugin.

Finally, submit your XML file on the dedicated page of the plugins catalog (registration is required).

Note

Path to plugin XML file must display the raw XML file itself. For example, the following URL for the exmple plugin would be incorrect:

https://github.com/pluginsGLPI/example/blob/main/example.xml

The correct one (use Github UI raw button) would be:

https://raw.githubusercontent.com/pluginsGLPI/example/refs/heads/main/example.xml

Teclibโ€™ will receive a notification for this submission and after some checks, will activate the publication on the catalog.

Miscellaneousยถ

Querying databaseยถ

Rely on DBmysqlIterator. It provides an exhaustive query builder.

 1<?php
 2
 3
 4// => SELECT * FROM `glpi_computers`
 5$iterator = $DB->request(['FROM' => 'glpi_computers']);
 6foreach ($ierator as $row) {
 7    //... work on each row ...
 8}
 9
10$DB->request([
11    'FROM' => ['glpi_computers', 'glpi_computerdisks'],
12    'LEFT JOIN' => [
13        'glpi_computerdisks' => [
14            'ON' => [
15                'glpi_computers' => 'id',
16                'glpi_computerdisks' => 'computer_id'
17            ]
18        ]
19    ]
20]);

Dashboardsยถ

Since GLPI 9.5, dashboards are available from:

  • Central page

  • Assets menu

  • Assistance menu

  • Ticket search results (mini dashboard)

This feature is split into several concepts - sub classes:

  • a placement grid (Glpi\Dashboard\Grid)

  • a widget collection (Glpi\Dashboard\Widget) to graphically display data

  • a data provider collection (Glpi\Dashboard\Provider) that queries the database

  • rights (Glpi\Dashboard\Right) on each dashboard

  • filters (Glpi\Dashboard\Filter) that can be displayed in a dashboard header and impacting providers.

With these classes, we can build a dashboard that will display cards on its grid. A card is a combination of a widget, a data provider, a place on grid and various options (like a background colour for example).

Completing existing conceptsยถ

From your plugin, you can complete these concepts with your own data and code.

๐Ÿ—‹ setup.php

 1<?php
 2
 3use Glpi\Plugin\Hooks;
 4use GlpiPlugin\Myplugin\Dashboard;
 5
 6function plugin_init_myplugin()
 7{
 8    ...
 9
10    // add new widgets to the dashboard
11    $PLUGIN_HOOKS[Hooks::DASHBOARD_TYPES]['myplugin'] = [
12        Dashboard::class => 'getTypes',
13    ];
14
15    // add new cards to the dashboard
16    $PLUGIN_HOOKS[Hooks::DASHBOARD_CARDS]['myplugin'] = [
17        Dashboard::class => 'getCards',
18    ];
19}

We will create a dedicated class for our dashboards:

๐Ÿ—‹ src/Dashboard.php

  1<?php
  2
  3namespace GlpiPlugin\Myplugin;
  4
  5class Dashboard
  6{
  7    static function getTypes()
  8    {
  9        return [
 10            'example' => [
 11                'label'    => __("Plugin Example", 'myplugin'),
 12                'function' => __class__ . "::cardWidget",
 13                'image'    => "https://via.placeholder.com/100x86?text=example",
 14            ],
 15            'example_static' => [
 16                'label'    => __("Plugin Example (static)", 'myplugin'),
 17                'function' => __class__ . "::cardWidgetWithoutProvider",
 18                'image'    => "https://via.placeholder.com/100x86?text=example+static",
 19            ],
 20        ];
 21    }
 22
 23    static function getCards($cards = [])
 24    {
 25        if (is_null($cards)) {
 26            $cards = [];
 27        }
 28        $new_cards =  [
 29            'plugin_example_card' => [
 30                'widgettype'   => ["example"],
 31                'label'        => __("Plugin Example card"),
 32                'provider'     => "PluginExampleExample::cardDataProvider",
 33            ],
 34            'plugin_example_card_without_provider' => [
 35                'widgettype'   => ["example_static"],
 36                'label'        => __("Plugin Example card without provider"),
 37            ],
 38            'plugin_example_card_with_core_widget' => [
 39                'widgettype'   => ["bigNumber"],
 40                'label'        => __("Plugin Example card with core provider"),
 41                'provider'     => "PluginExampleExample::cardBigNumberProvider",
 42            ],
 43        ];
 44
 45        return array_merge($cards, $new_cards);
 46   }
 47
 48    static function cardWidget(array $params = [])
 49    {
 50        $default = [
 51            'data'  => [],
 52            'title' => '',
 53            // this property is "pretty" mandatory,
 54            // as it contains the colors selected when adding widget on the grid send
 55            // without it, your card will be transparent
 56            'color' => '',
 57        ];
 58        $p = array_merge($default, $params);
 59
 60        // you need to encapsulate your html in div.card to benefit core style
 61        $html = "<div class='card' style='background-color: {$p["color"]};'>";
 62        $html.= "<h2>{$p['title']}</h2>";
 63        $html.= "<ul>";
 64        foreach ($p['data'] as $line) {
 65            $html.= "<li>$line</li>";
 66        }
 67        $html.= "</ul>";
 68        $html.= "</div>";
 69
 70        return $html;
 71    }
 72
 73    static function cardWidgetWithoutProvider(array $params = [])
 74    {
 75      $default = [
 76         // this property is "pretty" mandatory,
 77         // as it contains the colors selected when adding widget on the grid send
 78         // without it, your card will be transparent
 79         'color' => '',
 80      ];
 81      $p = array_merge($default, $params);
 82
 83      // you need to encapsulate your html in div.card to benefit core style
 84      $html = "<div class='card' style='background-color: {$p["color"]};'>
 85                  static html (+optional javascript) as card is not matched with a data provider
 86                  <img src='https://www.linux.org/images/logo.png'>
 87               </div>";
 88
 89      return $html;
 90   }
 91
 92    static function cardBigNumberProvider(array $params = [])
 93    {
 94        $default_params = [
 95            'label' => null,
 96            'icon'  => null,
 97        ];
 98        $params = array_merge($default_params, $params);
 99
100        return [
101            'number' => rand(),
102            'url'    => "https://www.linux.org/",
103            'label'  => "plugin example - some text",
104            'icon'   => "fab fa-linux", // font awesome icon
105        ];
106   }
107}

A few explanations on those methods:

  • getTypes(): define available widgets for cards and methods to call for display.

  • getCards(): define available cards for dashboards (when added to the grid). As previously explained, each is defined from a label, widget and optional data provider (from core or your plugin) combination

  • cardWidget(): use provided parameters to display HTML. You are free to delegate display to a Twig template, and use your favourite JavaScript library.

  • cardWidgetWithoutProvider(): almost the same as the cardWidget(), but does not use parameters and just returns a static HTML.

  • cardBigNumberProvider(): provider and expected return example when grid will display card.

Display your own dashboardยถ

GLPI dashboards system is modular and you can use it in your own displays.

1<?php
2
3use Glpi\Dashboard\Grid;
4
5$dashboard = new Grid('myplugin_example_dashboard', 10, 10, 'myplugin');
6$dashboard->show();

By adding a context (myplugin), you can filter dashboards available in the dropdown list at the top right of the grid. You will not see GLPI core ones (central, assistance, etc.).

Translating your pluginsยถ

In many places in current document, code exmaples takes care of using gettext GLPI notations to display strings to users. Even if your plugin will be private, it is a good practice to keep this gettext usage.

See developper guide translation documentation for more explanations and list of PHP functions that can be used.

  • On your local instance, you can use software like poedit to manage your translations.

  • You can also rely on online services like Transifex or Weblate (both are free for open source projects).

If you have used the Empty plugin skeleton, you will benefit from command line tools to manage your locales:

# extract strings to translate from your source code
# and put them in the locales/myplugin.pot file
vendor/bin/extract-locales

Warning

โ„น๏ธ It is possible your translations are not updated after compiling MO files, a restart of your PHP (or web server, depending on your configuration) may be required.

REST APIยถ

Since GLPI (since 9.1 release) has an external API in REST format. An XMLRPC format is also still available, but is deprecated.

API configuration

Configurationยถ

For security reasons, API is disabled bu default. From the Setup > General, API tab menu, you can enable it.

Itโ€™s available from your instance at:

  • http://path/to/glpi/apirest.php

  • http://path/to/glpi/apixmlrpc.php

The first link includes an integrated documentation when you access it from a simple browser (a link is provided as soon as the API is active).

For the rest of the configuration:

  • login allows to use login / password as well as web interface

  • token connection use the token displayed in user preferences

    external token
  • API clients allow to limit API access from some IP addresses and log if necessary. A client allowing access from any IP is provided by default.


You can use the API usage bootstrap. This one is written in PHP and relies on Guzzle library to handle HTTP requests.

By default, it does a connection with login details defined in the config.inc.php file (that you must create by copying the config.inc.example file).

Warning

โš ๏ธ Make sure the script is working as expected before continuing.

API usageยถ

To learn this part, with the help of integrated documentation (or latest stable GLPI API documentation on github), we will do several exercises:

Note

๐Ÿ“ Exercise: Test a new connection using GLPI user external token

Note

๐Ÿ“ Exercise: Close the session at the end of your script.

Note

๐Ÿ“ Exercise: Simulate computer life cycle:

  • add a computer and some volumes (Item_Disk),

  • edit several fields,

  • add commercial and administrative information (Infocom),

  • display its detail in a PHP page,

  • put it in the trashbin,

  • and then remove it completely.

Note

๐Ÿ“ Exercise: Retrieve computers list and display them an HTML array. The endpoint to use us โ€œSearch itemsโ€. If you want to display columns labels, you will have to use the โ€œList searchOptionsโ€ endpoint.


Creative Commons License