SmarMaker - Documentation
Docs» 15_training:module8-backend-api:controllers

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 :

  • $db : connexion base de données Dolibarr
  • $user : utilisateur Dolibarr connecté (via JWT)
  • $conf : configuration Dolibarr
  • $langs : traductions

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 →

Previous Next

Made with ❤ by CAP-REL · SmartMaker · GNU AGPL v3+
Code source · Faire un don
SmarMaker - Documentation
Traductions de cette page:
  • Français
  • Deutsch
  • English
  • Español
  • Italiano
  • Nederlands

Table of Contents

Table des matières

  • Chapitre 2 : Controllers
    • Structure d'un controller
    • Variables globales disponibles
    • CRUD complet
    • Recherche avec filtres
    • Gestion des fichiers
    • Vérification des droits
    • Logging
    • Points clés à retenir
  • SmartAuth
  • SmartMaker - Back (PHP)
    • Mapping Dolibarr - React
  • SmartMaker - Front (React)
    • Animations de pages
    • Architecture
    • Astuces
    • Calendar
    • Composants et pages
    • Configuration du Provider
    • Debug et Logs
    • Hooks SmartCommon
    • PWA (Progressive Web App)
    • Requêtes API
    • Routage
    • SmartCommon
    • Stockage de données
    • Thèmes
    • Traductions
  • HowTo - Pas à pas - Votre première application
    • Développement PHP (back)
    • Développement React (front)
    • Première étape : Module Builder Dolibarr
    • SmartAuth
    • SmartBoot : Un "squelette" quasiment prêt à l'emploi
  • Formation SmartMaker
    • Module 1 : Fondamentaux JavaScript ES6+
      • Chapitre 1 : Variables et Scope
      • Chapitre 2 : Fonctions
      • Chapitre 3 : Programmation Asynchrone
      • Chapitre 4 : Modules ES6
    • Module 2 : Introduction à React
      • Chapitre 1 : Philosophie React
      • Chapitre 2 : JSX
      • Chapitre 3 : Composants
    • Module 3 : Hooks React Fondamentaux
      • Chapitre 1 : useState
      • Chapitre 2 : useEffect
      • Chapitre 3 : useRef
      • Chapitre 4 : useContext
    • Module 4 : React Avancé
      • Chapitre 1 : useCallback et useMemo
      • Chapitre 2 : Custom Hooks
      • Chapitre 3 : Redux et Redux Toolkit
    • Module 5 : Architecture SmartMaker
      • Chapitre 1 : Structure du projet
      • Chapitre 2 : Configuration
      • Chapitre 3 : Flux de données
    • Module 6 : SmartCommon - Composants
      • Chapitre 1 : Mise en page
      • Chapitre 2 : Navigation
      • Chapitre 3 : Formulaires
      • Chapitre 4 : Affichage
    • Module 7 : SmartCommon - Hooks
      • Chapitre 1 : useApi
      • Chapitre 2 : Gestion d'état
      • Chapitre 3 : Hooks utilitaires
    • Module 8 : Backend API (PHP)
      • Chapitre 1 : Routage
      • Chapitre 2 : Controllers
      • Chapitre 3 : Mappers
      • Extrafields et formulaires dynamiques
    • Module 9 : Intégration complète
      • Chapitre 1 : Backend
      • Chapitre 2 : Frontend
      • Chapitre 3 : Déploiement
    • Module 10 : Fonctionnalités avancées
      • Chapitre 1 : Mode offline
      • Chapitre 2 : Internationalisation (i18n)
      • Chapitre 3 : Autres fonctionnalités
    • Module 11 : Bonnes pratiques
  • Démonstration
  • Start
  • Composants et pages
  • Afficher le texte source
  • Anciennes révisions
  • Liens de retour
  • Haut de page