Table des matières

Chapitre 2 : Controllers

Les controllers contiennent la logique métier de l'API.

Structure d'un controller

snippet.php
<?php
// smartmaker-api/Controllers/ItemController.php
 
namespace MonModule\Api;
 
class ItemController
{
    public function __construct() {}
 
    /**
     * Liste des items
     * @param array|null $payload
     * @return array [data, httpCode]
     */
    public function index($payload = null)
    {
        global $db, $user;
 
        // Logique ici
 
        return [['items' => $items], 200];
    }
}

Variables globales disponibles

Dans les routes protégées :

CRUD complet

snippet.php
<?php
namespace MonModule\Api;
 
use MonModule\Api\Mappers\dmProduct;
 
class ProductController
{
    /**
     * Liste des produits
     */
    public function index($payload = null)
    {
        global $db, $user;
 
        require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';
 
        // Paramètres de pagination
        $limit = $payload['limit'] ?? 50;
        $offset = $payload['offset'] ?? 0;
 
        // Requête SQL
        $sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "product";
        $sql .= " WHERE entity IN (" . getEntity('product') . ")";
        $sql .= " ORDER BY label ASC";
        $sql .= " LIMIT " . (int) $limit;
        $sql .= " OFFSET " . (int) $offset;
 
        $resql = $db->query($sql);
        $products = [];
 
        if ($resql) {
            $mapper = new dmProduct();
 
            while ($obj = $db->fetch_object($resql)) {
                $product = new \Product($db);
                $product->fetch($obj->rowid);
 
                $products[] = $mapper->exportMappedData($product);
            }
        }
 
        return [['products' => $products], 200];
    }
 
    /**
     * Détail d'un produit
     */
    public function show($payload = null)
    {
        global $db;
 
        $id = $payload['id'] ?? null;
 
        if (!$id) {
            return ['ID required', 400];
        }
 
        require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';
 
        $product = new \Product($db);
        $res = $product->fetch($id);
 
        if ($res <= 0) {
            return ['Product not found', 404];
        }
 
        // Charger les extrafields
        $product->fetch_optionals();
 
        $mapper = new dmProduct();
        $data = $mapper->exportMappedData($product);
 
        return [$data, 200];
    }
 
    /**
     * Créer un produit
     */
    public function create($payload = null)
    {
        global $db, $user;
 
        // Validation
        if (empty($payload['label'])) {
            return ['Label required', 400];
        }
 
        require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';
 
        $product = new \Product($db);
        $product->ref = $payload['ref'] ?? '';
        $product->label = $payload['label'];
        $product->description = $payload['description'] ?? '';
        $product->price = $payload['price'] ?? 0;
        $product->status = $payload['status'] ?? 1;
 
        // Créer en base
        $res = $product->create($user);
 
        if ($res < 0) {
            return ['Error creating product: ' . $product->error, 500];
        }
 
        // Sauvegarder les extrafields
        if (!empty($payload['extrafields'])) {
            foreach ($payload['extrafields'] as $key => $value) {
                $product->array_options['options_' . $key] = $value;
            }
            $product->insertExtraFields();
        }
 
        return [['id' => $res], 201];
    }
 
    /**
     * Mettre à jour un produit
     */
    public function update($payload = null)
    {
        global $db, $user;
 
        $id = $payload['id'] ?? null;
 
        if (!$id) {
            return ['ID required', 400];
        }
 
        require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';
 
        $product = new \Product($db);
        $res = $product->fetch($id);
 
        if ($res <= 0) {
            return ['Product not found', 404];
        }
 
        // Mettre à jour les champs fournis
        if (isset($payload['label'])) {
            $product->label = $payload['label'];
        }
        if (isset($payload['description'])) {
            $product->description = $payload['description'];
        }
        if (isset($payload['price'])) {
            $product->price = $payload['price'];
        }
        if (isset($payload['status'])) {
            $product->status = $payload['status'];
        }
 
        $res = $product->update($product->id, $user);
 
        if ($res < 0) {
            return ['Error updating product: ' . $product->error, 500];
        }
 
        // Mettre à jour les extrafields
        if (!empty($payload['extrafields'])) {
            $product->fetch_optionals();
            foreach ($payload['extrafields'] as $key => $value) {
                $product->array_options['options_' . $key] = $value;
            }
            $product->updateExtraFields();
        }
 
        return ['Updated', 200];
    }
 
    /**
     * Supprimer un produit
     */
    public function delete($payload = null)
    {
        global $db, $user;
 
        $id = $payload['id'] ?? null;
 
        if (!$id) {
            return ['ID required', 400];
        }
 
        require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';
 
        $product = new \Product($db);
        $res = $product->fetch($id);
 
        if ($res <= 0) {
            return ['Product not found', 404];
        }
 
        // Vérifier les droits
        if (!$user->hasRight('produit', 'supprimer')) {
            return ['Permission denied', 403];
        }
 
        $res = $product->delete($user);
 
        if ($res < 0) {
            return ['Error deleting product: ' . $product->error, 500];
        }
 
        return ['Deleted', 200];
    }
}

Recherche avec filtres

snippet.php
/**
 * Recherche avancée
 */
public function search($payload = null)
{
    global $db;
 
    require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';
 
    // Filtres
    $search = $payload['search'] ?? '';
    $category = $payload['category'] ?? null;
    $status = $payload['status'] ?? null;
    $minPrice = $payload['minPrice'] ?? null;
    $maxPrice = $payload['maxPrice'] ?? null;
 
    // Pagination
    $limit = min($payload['limit'] ?? 50, 100);  // Max 100
    $offset = $payload['offset'] ?? 0;
 
    // Construction de la requête
    $sql = "SELECT p.rowid FROM " . MAIN_DB_PREFIX . "product as p";
 
    // Jointure catégorie si nécessaire
    if ($category) {
        $sql .= " INNER JOIN " . MAIN_DB_PREFIX . "categorie_product as cp";
        $sql .= " ON p.rowid = cp.fk_product";
    }
 
    $sql .= " WHERE p.entity IN (" . getEntity('product') . ")";
 
    // Filtres
    if ($search) {
        $sql .= " AND (p.label LIKE '%" . $db->escape($search) . "%'";
        $sql .= " OR p.ref LIKE '%" . $db->escape($search) . "%')";
    }
 
    if ($category) {
        $sql .= " AND cp.fk_categorie = " . (int) $category;
    }
 
    if ($status !== null) {
        $sql .= " AND p.tosell = " . (int) $status;
    }
 
    if ($minPrice !== null) {
        $sql .= " AND p.price >= " . (float) $minPrice;
    }
 
    if ($maxPrice !== null) {
        $sql .= " AND p.price <= " . (float) $maxPrice;
    }
 
    $sql .= " ORDER BY p.label ASC";
    $sql .= " LIMIT " . (int) $limit;
    $sql .= " OFFSET " . (int) $offset;
 
    $resql = $db->query($sql);
    $products = [];
    $mapper = new dmProduct();
 
    while ($obj = $db->fetch_object($resql)) {
        $product = new \Product($db);
        $product->fetch($obj->rowid);
        $products[] = $mapper->exportMappedData($product);
    }
 
    return [['products' => $products], 200];
}

Gestion des fichiers

snippet.php
/**
 * Télécharger un fichier
 */
public function download($payload = null)
{
    global $conf, $db;
 
    $element = $payload['element'];     // ex: 'product'
    $parentId = $payload['parentId'];
    $ref = $payload['ref'];             // nom du fichier
 
    // Construire le chemin
    $dir = $conf->$element->dir_output;
    $filepath = $dir . '/' . $parentId . '/' . $ref;
 
    if (!file_exists($filepath)) {
        return ['File not found', 404];
    }
 
    // Retourner le fichier en base64
    $content = file_get_contents($filepath);
    $mime = mime_content_type($filepath);
    $base64 = base64_encode($content);
 
    return [[
        'filename' => $ref,
        'mime' => $mime,
        'content' => 'data:' . $mime . ';base64,' . $base64
    ], 200];
}
 
/**
 * Uploader un fichier
 */
public function upload($payload = null)
{
    global $conf, $user;
 
    $element = $payload['element'];
    $parentId = $payload['parentId'];
    $filename = $payload['filename'];
    $content = $payload['content'];  // base64
 
    // Décoder le contenu
    $data = base64_decode(preg_replace('#^data:.+;base64,#', '', $content));
 
    // Chemin de destination
    $dir = $conf->$element->dir_output . '/' . $parentId;
 
    // Créer le dossier si nécessaire
    if (!is_dir($dir)) {
        dol_mkdir($dir);
    }
 
    $filepath = $dir . '/' . $filename;
 
    // Sauvegarder
    if (file_put_contents($filepath, $data) === false) {
        return ['Error saving file', 500];
    }
 
    return [['path' => $filepath], 201];
}

Vérification des droits

snippet.php
public function delete($payload = null)
{
    global $db, $user;
 
    // Vérifier un droit spécifique
    if (!$user->hasRight('monmodule', 'delete')) {
        return ['Permission denied', 403];
    }
 
    // Vérifier si admin
    if (!$user->admin) {
        return ['Admin required', 403];
    }
 
    // Suite du code...
}

Logging

snippet.php
public function create($payload = null)
{
    global $db, $user;
 
    // Log pour debug
    dol_syslog("ProductController::create by " . $user->login, LOG_DEBUG);
 
    // En cas d'erreur
    dol_syslog("ProductController::create error: " . $product->error, LOG_ERR);
 
    // ...
}

Points clés à retenir

  1. Retourner [data, code] : toujours un tableau avec données et code HTTP
  2. $payload contient les paramètres d'URL et le body JSON
  3. Utiliser les mappers pour convertir les objets Dolibarr
  4. Valider les entrées avant traitement
  5. Vérifier les droits pour les actions sensibles

← Chapitre précédent | Retour au module | Chapitre suivant : Mappers →