Indice

Développement PHP (back)

SmartBoot ajoute à votre module Dolibarr toute la pile technique pour exposer une API REST.

Structure des fichiers

Après SmartBoot, votre module contient :

monmodule/
├── smartmaker-api-prepend.php    # Fichier d'initialisation
├── pwa/
│   ├── api.php                   # Routeur API
│   └── .htaccess                 # Redirection Apache
└── smartmaker-api/
    └── Controllers/              # Vos contrôleurs

smartmaker-api-prepend.php

Ce fichier initialise l'environnement SmartMaker :

<note important>Ne modifiez pas ce fichier sauf si vous savez ce que vous faites.</note>

pwa/api.php - Le routeur

Syntaxe des routes

Route::action(path, Controller::class, method, protected);
Paramètre Description
action get, post, put, delete
path Chemin de l'API (ex: items, items/{id})
Controller::class Classe PHP à appeler
method Méthode de la classe
protected true = authentification requise

Exemple complet

<?php
require_once '../smartmaker-api-prepend.php';

use SmartAuth\Api\AuthController;
use SmartAuth\Api\RouteController as Route;
use MonModule\Api\ItemController;
use MonModule\Api\FileController;

// === Routes d'authentification ===
Route::get('login',     AuthController::class, 'index');       // Infos connexion
Route::post('login',    AuthController::class, 'login');       // Login
Route::get('refresh',   AuthController::class, 'refresh');     // Refresh token
Route::post('logout',   AuthController::class, 'logout', true);// Logout
Route::post('device',   AuthController::class, 'device', true);// Enregistrer device

// === Routes métier ===
// CRUD Items
Route::get('items',          ItemController::class, 'index', true);
Route::get('items/{id}',     ItemController::class, 'show', true);
Route::post('items',         ItemController::class, 'create', true);
Route::put('items/{id}',     ItemController::class, 'update', true);
Route::delete('items/{id}',  ItemController::class, 'delete', true);

// Recherche avec filtres
Route::post('items/search/{filter}', ItemController::class, 'search', true);

// Fichiers
Route::get('file/{element}/{id}/{filename}', FileController::class, 'download', true);

// Fallback - Accès refusé
json_reply('Access denied', 403);

Paramètres dynamiques

Les segments entre accolades sont passés dans le payload :

// Route: GET /items/123
Route::get('items/{id}', ItemController::class, 'show', true);

// Dans le controller:
public function show($payload)
{
    $id = $payload['id']; // 123
}

pwa/.htaccess

Redirige toutes les requêtes vers api.php (Apache >= 2.2.16) :

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ api.php [QSA,L]

<note>Pour nginx, une configuration équivalente est nécessaire dans le fichier de configuration du serveur.</note>

Créer un Controller

Structure de base

<?php
// smartmaker-api/Controllers/ItemController.php

namespace MonModule\Api;

class ItemController
{
    /**
     * Liste des items
     * GET /items
     */
    public function index($payload = null)
    {
        global $db, $user;

        $items = [];
        $sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "monobject";
        $sql .= " WHERE entity = " . $user->entity;
        $sql .= " ORDER BY date_creation DESC";

        $resql = $db->query($sql);

        while ($obj = $db->fetch_object($resql)) {
            $item = new \MonObject($db);
            $item->fetch($obj->rowid);

            $mapping = new dmMonObject();
            $items[] = $mapping->exportMappedData($item);
        }

        return [$items, 200];
    }

    /**
     * Détail d'un item
     * GET /items/{id}
     */
    public function show($payload = null)
    {
        global $db;

        $id = $payload['id'] ?? null;

        if (!$id) {
            return ['ID required', 400];
        }

        $item = new \MonObject($db);
        $res = $item->fetch($id);

        if ($res <= 0) {
            return ['Not found', 404];
        }

        $item->fetch_optionals();
        $item->fetch_lines();

        $mapping = new dmMonObject();
        $data = $mapping->exportMappedData($item);

        return [$data, 200];
    }

    /**
     * Créer un item
     * POST /items
     */
    public function create($payload = null)
    {
        global $db, $user;

        $item = new \MonObject($db);
        $item->label = $payload['label'] ?? '';
        $item->description = $payload['description'] ?? '';
        $item->fk_user = $user->id;

        $res = $item->create($user);

        if ($res < 0) {
            return [$item->error ?: 'Creation failed', 500];
        }

        return [['id' => $res], 201];
    }

    /**
     * Modifier un item
     * PUT /items/{id}
     */
    public function update($payload = null)
    {
        global $db, $user;

        $id = $payload['id'] ?? null;

        $item = new \MonObject($db);
        $res = $item->fetch($id);

        if ($res <= 0) {
            return ['Not found', 404];
        }

        // Mettre à jour les champs fournis
        if (isset($payload['label'])) {
            $item->label = $payload['label'];
        }
        if (isset($payload['description'])) {
            $item->description = $payload['description'];
        }

        $res = $item->update($user);

        if ($res < 0) {
            return [$item->error ?: 'Update failed', 500];
        }

        return ['Updated', 200];
    }

    /**
     * Supprimer un item
     * DELETE /items/{id}
     */
    public function delete($payload = null)
    {
        global $db, $user;

        $id = $payload['id'] ?? null;

        $item = new \MonObject($db);
        $res = $item->fetch($id);

        if ($res <= 0) {
            return ['Not found', 404];
        }

        $res = $item->delete($user);

        if ($res < 0) {
            return [$item->error ?: 'Delete failed', 500];
        }

        return ['Deleted', 200];
    }

    /**
     * Recherche avec filtres
     * POST /items/search/{filter}
     */
    public function search($payload = null)
    {
        global $db, $user;

        $filter = $payload['filter'] ?? 'all';
        $limit = $payload['limit'] ?? 20;
        $offset = $payload['offset'] ?? 0;

        $sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "monobject";
        $sql .= " WHERE entity = " . $user->entity;

        // Appliquer les filtres
        switch ($filter) {
            case 'active':
                $sql .= " AND status = 1";
                break;
            case 'draft':
                $sql .= " AND status = 0";
                break;
            case 'mine':
                $sql .= " AND fk_user = " . $user->id;
                break;
        }

        // Recherche texte
        if (!empty($payload['search'])) {
            $search = $db->escape($payload['search']);
            $sql .= " AND (label LIKE '%$search%' OR description LIKE '%$search%')";
        }

        $sql .= " ORDER BY date_creation DESC";
        $sql .= " LIMIT " . (int)$limit . " OFFSET " . (int)$offset;

        $resql = $db->query($sql);
        $items = [];

        while ($obj = $db->fetch_object($resql)) {
            $item = new \MonObject($db);
            $item->fetch($obj->rowid);

            $mapping = new dmMonObject();
            $items[] = $mapping->exportMappedData($item);
        }

        return [$items, 200];
    }
}

Accès à l'utilisateur connecté

L'utilisateur JWT est disponible via $payload['user'] ou la variable globale $user :

public function create($payload = null)
{
    global $user;

    // $user est l'objet User Dolibarr complet
    dol_syslog("Action par: " . $user->login);

    // Récupérer l'ID
    $userId = $user->id;

    // Vérifier les droits
    if (!$user->rights->monmodule->write) {
        return ['Permission denied', 403];
    }
}

Créer une classe de mapping (dm*)

Les classes dm* transforment les objets Dolibarr en JSON pour React :

<?php
// smartmaker-api/dmMonObject.php

namespace MonModule\Api;

class dmMonObject extends \SmartAuth\DolibarrMapping\dmBase
{
    use \SmartAuth\DolibarrMapping\dmTrait;

    protected $type = "object";
    protected $parentClassName = 'MonObject';

    // Pour les extrafields
    protected $parentClassToUseForExtraFields = "MonObject";
    protected $parentElementToUseForExtraFields = "monobject";

    // Mapping: champ Dolibarr => champ JSON
    protected $listOfPublishedFields = [
        'rowid'         => 'id',
        'ref'           => 'ref',
        'label'         => 'label',
        'description'   => 'description',
        'fk_soc'        => 'thirdparty',    // Résolu automatiquement
        'fk_statut'     => 'status',
        'date_creation' => 'created_at',

        // Extrafields
        'options_myfield' => 'my_field',
    ];

    public function __construct()
    {
        global $langs;
        $langs->load("monmodule@monmodule");
        $this->boot();
    }

    /**
     * Transformer une valeur avant l'envoi
     * Méthode magique: fieldFilterValue + NomDuChamp
     */
    public function fieldFilterValueCreatedAt($object)
    {
        return dol_print_date($object->date_creation, 'dayhour');
    }
}

Voir aussi