---
title: "Mapping Dolibarr - React"
weight: 30
---

# Mapping Dolibarr - React

Pour exposer les objets Dolibarr vers l'application React, SmartMaker utilise des classes de mapping préfixées `dm`. Un mapper décrit **quels champs sont publiés**, **sous quel nom** et **lesquels sont modifiables**.

## Avant d'écrire un mapper : vérifiez qu'il n'existe pas déjà

SmartAuth livre une **bibliothèque de mappers pour les objets du coeur Dolibarr**, dans `smartauth/dolMapping/`. Ne réécrivez pas le vôtre pour un objet standard.

| Domaine | Mappers disponibles |
| --- | --- |
| Documents commerciaux | `dmInvoice`, `dmOrder`, `dmProposal`, `dmContract`, `dmSupplierInvoice`, `dmSupplierOrder`, `dmSupplierProposal`, `dmShipment`, `dmReception`, `dmDeliveryNote` |
| Tiers et contacts | `dmThirdparty`, `dmContact`, `dmSupplier`, `dmUser` |
| Catalogue et stock | `dmProduct`, `dmCategory`, `dmWarehouse`, `dmStockMovement` |
| Production | `dmMo`, `dmBom` |
| Adhérents | `dmMember`, `dmMemberType`, `dmSubscription`, `dmDonation` |
| Projet et planning | `dmProject`, `dmTask`, `dmAgendaEvent`, `dmIntervention`, `dmExpenseReport`, `dmTicket` |
| Comptabilité | `dmBankAccount`, `dmBank`, `dmCompanyBankAccount`, `dmMulticurrency` |
| Dictionnaires | `dmC*` : `dmCcountry`, `dmCstate`, `dmCpaymentterm`, `dmCunits`, etc. (lecture seule) |

> [!IMPORTANT]
> Un module n'écrit un mapper que pour **ses objets métier propres**. Pour un objet du coeur, il consomme le mapper SmartAuth, éventuellement en en héritant. Un bug corrigé dans `dolMapping/` profite à tous les modules ; un mapper dupliqué localement recrée la dette que la centralisation a supprimée.

### Et souvent, pas de mapper du tout

Pour les objets du coeur, SmartAuth expose une **façade REST générique** qui s'appuie sur ces mappers. Un module n'a alors ni CRUD, ni recherche, ni pagination à écrire :

```
GET    objects/{objtype}                liste paginee (filtres, tri, recherche)
GET    objects/{objtype}/describe       schema des champs (objectDesc)
GET    objects/{objtype}/{id}           un objet
POST   objects/{objtype}                creation
PATCH  objects/{objtype}/{id}           mise a jour
DELETE objects/{objtype}/{id}           suppression
```

Les documents à lignes ajoutent `objects/{objtype}/{id}/lines`, les workflows `objects/{objtype}/{id}/actions/{action}`, les factures `objects/{objtype}/{id}/payments`.

Écrivez donc un mapper quand vous avez **votre propre classe Dolibarr** à exposer. C'est le cas traité dans le reste de cette page.

## Déclarer un mapper

### Structure minimale

```
<?php
namespace MyModule\Api;

// La classe Dolibarr cible DOIT etre chargee ici
dol_include_once('/mymodule/class/myobject.class.php');

use SmartAuth\DolibarrMapping\dmBase;
use SmartAuth\DolibarrMapping\dmTrait;

class dmMyObject extends dmBase
{
    use dmTrait;

    // Type de mapper : 'object' ou 'dict'
    protected $type = 'object';

    // Classe Dolibarr representee : OBLIGATOIRE
    protected $dolibarrClassName = 'MyObject';

    // Element pour les extrafields (colonne elementtype de llx_extrafields)
    protected $parentTableElementToUseForExtraFields = 'myobject';

    // Mapping : nom Dolibarr => nom API
    protected $listOfPublishedFields = [
        'rowid'         => 'id',
        'ref'           => 'ref',
        'label'         => 'label',
        'description'   => 'description',
        'fk_soc'        => 'thirdparty',
        'fk_statut'     => 'status',
        'date_creation' => 'created_at',
        'note_public'   => 'public_note',
        'note_private'  => 'private_note',
    ];

    // Allowlist d'ecriture : noms DOLIBARR, jamais noms API
    protected $writableFields = [
        'label',
        'description',
        'note_public',
    ];

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

L'appel à `boot()` en fin de constructeur est obligatoire.

### Propriétés reconnues

| Propriété | Obligatoire | Rôle |
| --- | --- | --- |
| `$type` | oui | `object` ou `dict` |
| `$dolibarrClassName` | oui pour `object` | nom exact de la classe Dolibarr représentée |
| `$listOfPublishedFields` | oui | map nom Dolibarr vers nom API |
| `$writableFields` | non, défaut `[]` | allowlist d'écriture, en noms Dolibarr |
| `$listOfDerivedFields` | non | champs calculés sans colonne source |
| `$parentTableElementToUseForExtraFields` | non | rattachement des extrafields |
| `$parentClassName` | non | **uniquement** pour un mapper de sous-objet ou de ligne |
| `$parentClassNameForLines` | non | classe Dolibarr des lignes |
| `$listOfPublishedFieldsForLines` | non | map des champs de lignes |
| `$parentLabelForLines` | non | clé API sous laquelle les lignes sont publiées |
| `$parentFieldsOverride` | non | patch de la définition d'un champ (type, required...) |

### Piège : $dolibarrClassName n'est pas déduit du nom du mapper

> [!WARNING]
> `$dolibarrClassName` est **obligatoire** sur tout mapper de type `object`. Il n'est pas déduit du nom de la classe, et pour cause : la déduction serait fausse dans la plupart des cas.

| Mapper | Classe Dolibarr réelle | Ce qu'une déduction donnerait |
| --- | --- | --- |
| `dmThirdparty` | `Societe` | `Thirdparty` |
| `dmInvoice` | `Facture` | `Invoice` |
| `dmOrder` | `Commande` | `Order` |
| `dmProposal` | `Propal` | `Proposal` |
| `dmIntervention` | `Fichinter` | `Intervention` |
| `dmWarehouse` | `Entrepot` | `Warehouse` |
| `dmMember` | `Adherent` | `Member` |
| `dmShipment` | `Expedition` | `Shipment` |

Le mapper valide sa déclaration au `boot()` et lève une `LogicException` explicite si `$dolibarrClassName` manque, ou si la classe annoncée n'existe pas (typiquement, un `dol_include_once` oublié).

### Piège : ne jamais utiliser $parentClassName sur un objet de premier niveau

`$parentClassName` sert **uniquement** à un mapper de sous-objet ou de ligne, pour remonter à son parent. Un mapper d'en-tête ne doit pas le déclarer.

```
// FAUX : Product n'a pas de parent, et la propriete est mauvaise
class dmProduct extends dmBase
{
    use dmTrait;
    protected $parentClassName = 'Product';
}

// CORRECT
class dmProduct extends dmBase
{
    use dmTrait;
    protected $type = 'object';
    protected $dolibarrClassName = 'Product';
}

// CORRECT : mapper de ligne, avec un vrai parent
class dmFichinterLigne extends dmBase
{
    use dmTrait;
    protected $type = 'object';
    protected $dolibarrClassName = 'FichinterLigne';
    protected $parentClassName   = 'Fichinter';
}
```

Retenez la distinction en une phrase : `$dolibarrClassName` répond à "qui suis-je ?", `$parentClassName` à "qui est mon parent ?". Déclarer les deux identiques lève une `LogicException` au boot.

## Lecture : exportMappedData()

`exportMappedData()` convertit un objet Dolibarr en objet JSON aux noms de champs API.

```
$object = new \MyObject($db);
$object->fetch($id);
$object->fetch_optionals();  // extrafields
$object->fetch_lines();      // lignes, si l'objet en a

$mapper = new dmMyObject();
$payload = $mapper->exportMappedData($object);
```

Ce que la méthode fait au passage :
- elle résout les clés étrangères déclarées dans le `$fields` Dolibarr, sur deux niveaux de profondeur au maximum
- elle inclut les extrafields déclarés en `options_xxx`
- elle inclut les catégories associées quand l'objet en a (produit, tiers, contact, adhérent)
- elle expose `nb_linked_files`, et la liste complète des fichiers si `$mapper->withFiles = true`

### Décrire le schéma au front

`objectDesc()` renvoie la description structurelle de l'objet : champs, types API, libellés traduits, position d'affichage. C'est ce qui permet au front de générer formulaires et listes sans coder les champs en dur. Le résultat est calculé une fois au boot et mis en cache.

```
$mapper = new dmMyObject();
$schema = $mapper->objectDesc();
```

## Écriture : writableFields et importMappedData()

`importMappedData()` est l'inverse de `exportMappedData()` : il prend un payload API et renvoie un `stdClass` aux noms Dolibarr, prêt à être appliqué sur l'objet.

```
$mapper = new dmMyObject();

try {
    $sanitized = $mapper->importMappedData($payload);
} catch (\SmartAuth\DolibarrMapping\MapperValidationException $e) {
    return [['errors' => $e->getErrors()], 400];
}

$object = new \MyObject($db);
$object->fetch($id);
foreach (get_object_vars($sanitized) as $field => $value) {
    $object->$field = $value;
}
$object->update($user);
```

### Le contrat

  - tout champ absent de `$writableFields` est **rejeté**, pas ignoré
  - tous les rejets sont collectés et reportés en une seule exception, pas au premier venu
  - les noms API sont automatiquement réinversés en noms Dolibarr
  - chaque valeur est castée selon le type déclaré dans le `$fields` Dolibarr : `integer` vers `int`, `price` ou `double` vers `float`, `bool*` vers 0 ou 1, `date` vers timestamp
  - la clé `lines` lève toujours une exception : les lignes ne passent pas par `importMappedData()`

Un mapper qui ne déclare pas `$writableFields` est **entièrement en lecture seule**. C'est le comportement par défaut, et il est volontaire.

### Piège : writableFields contient des noms Dolibarr

> [!WARNING]
> Chaque entrée de `$writableFields` doit être une **clé** de `$listOfPublishedFields` (le nom Dolibarr), jamais une valeur (le nom API). C'est un bug silencieux : le champ est rejeté, aucune erreur n'est visible côté client, et l'objet n'est jamais mis à jour.

```
protected $listOfPublishedFields = [
    'nom'   => 'name',
    'email' => 'email',
];

// CORRECT
protected $writableFields = ['nom', 'email'];

// FAUX : 'name' est le nom API, pas le nom Dolibarr
protected $writableFields = ['name'];
```

Ce piège a été rencontré trois fois en production avant d'être verrouillé. Le boot lève désormais une `LogicException` listant les entrées fautives.

### Ce que importMappedData() ne fait pas
- pas de validation métier : le mapper assainit et caste, Dolibarr valide à la persistance
- pas d'écriture des lignes : passer par `addline()` et `updateline()`
- cast au mieux : une chaîne `"abc"` dans un champ entier devient `0`, sans avertissement

## Transformer une valeur : fieldFilterValueXxx()

Pour transformer un champ avant export, déclarez une méthode publique `fieldFilterValue` suivie du nom Dolibarr du champ en CamelCase. Le nom de la méthode **est** le contrat : pas d'annotation, pas d'enregistrement.

```
/**
 * Renvoie une URL signee au lieu du nom de fichier brut.
 */
public function fieldFilterValueLogo($object, $value)
{
    return '/upload/societe/' . $object->id . '/' . urlencode($value);
}

/**
 * Recupere les contacts associes.
 */
public function fieldFilterValueContacts($object, $value)
{
    return $object->liste_contact(-1, 'external');
}
```

Cas d'usage typiques : convertir un timestamp, traduire un code en libellé, calculer une valeur dérivée d'un autre champ.

### Champs dérivés sans colonne Dolibarr

Pour publier une clé qui n'est adossée à aucune colonne, déclarez-la dans `$listOfDerivedFields` et non dans `$listOfPublishedFields`. La méthode `fieldFilterValueXxx()` est alors appelée sans vérification préalable de la présence du champ source.

> [!WARNING]
> Ne renvoyez pas d'images en base64 dans un champ de liste. Une liste de 200 tiers avec leur logo inline sature la réponse et la base locale de la PWA. La convention est de publier une **URL de média** et, pour les listes, une vignette `logo_mini`.

## Extrafields

Les extrafields se publient comme des champs normaux, en préfixant leur nom par `options_` :

```
protected $listOfPublishedFields = [
    'options_mymodule_address'   => 'intervention_address',
    'options_mymodule_date_inter' => 'date_intervention',
];
```

Le rattachement se fait par `$parentTableElementToUseForExtraFields`, qui doit valoir exactement la colonne `elementtype` de `llx_extrafields` pour cet objet.

### Extrafields configurables par l'administrateur

Le squelette SmartBoot montre le pattern : deux constantes listent les extrafields à exposer, en lecture seule et en lecture-écriture, et le constructeur les ajoute au mapping.

```
public function __construct()
{
    global $db;
    $this->db = $db;

    $extRO = getDolGlobalString('MYMODULE_SMARTMAKER_EXTRAFIELDS_RO');
    if (!empty($extRO)) {
        foreach (explode(',', $extRO) as $field) {
            $field = trim($field);
            if (!empty($field)) {
                $key = 'options_' . $field;
                $this->listOfPublishedFields[$key] = $key;
            }
        }
    }

    $extRW = getDolGlobalString('MYMODULE_SMARTMAKER_EXTRAFIELDS_RW');
    if (!empty($extRW)) {
        foreach (explode(',', $extRW) as $field) {
            $field = trim($field);
            if (!empty($field)) {
                $key = 'options_' . $field;
                $this->listOfPublishedFields[$key] = $key;
                $this->writableFields[] = $key;
            }
        }
    }

    $this->boot();
}
```

> [!NOTE]
> Les extrafields eux-mêmes ne se créent jamais en SQL. Ils se déclarent via `$extrafields->addExtraField(...)` dans le `init()` du descripteur de module.

## Objets avec lignes

```
// Classe Dolibarr des lignes
protected $parentClassNameForLines = 'MyObjectLine';

// Description des champs de lignes, pour generer le formulaire
protected $parentFieldsForLines = [
    'id'   => ['type' => 'integer',  'label' => 'ID',          'visible' => -1, 'position' => 10],
    'date' => ['type' => 'datetime', 'label' => 'Date',        'visible' => 1,  'position' => 50],
    'desc' => ['type' => 'html',     'label' => 'Description', 'visible' => 1,  'position' => 105],
    'qty'  => ['type' => 'integer',  'label' => 'Quantity',    'visible' => 1,  'position' => 110],
];

// Mapping des champs de lignes
protected $listOfPublishedFieldsForLines = [
    'id'       => 'id',
    'date'     => 'date',
    'desc'     => 'description',
    'qty'      => 'quantity',
    'subprice' => 'unit_price',
    'total_ht' => 'total',
];

// Cle API sous laquelle les lignes sont publiees
protected $parentLabelForLines = "linesDetail";
```

Les lignes s'exposent en lecture par ce mécanisme. En écriture, elles passent par les méthodes Dolibarr natives `addline()`, `updateline()` et `deleteline()`, ou par les routes `objects/{objtype}/{id}/lines` de la façade pour les objets du coeur.

> [!NOTE]
> `fetch()` ne charge pas toujours les lignes. Appelez `fetch_lines()` avant l'export, sinon l'objet sort sans ses lignes.

## Ajuster la description d'un champ

`$parentFieldsOverride` patche la définition d'un champ remontée depuis Dolibarr, sans toucher à la classe en amont.

```
protected $parentFieldsOverride = [
    'duree'    => ['type' => 'duration', 'required' => 'required'],
    'contacts' => ['type' => 'array'],
    'fk_user'  => ['type' => 'select'],
];
```

Typiquement : rendre une durée stockée en secondes comme un champ `duration` côté front, ou imposer un champ que Dolibarr considère optionnel.

## Convention de nommage des champs API

Le mapper est l'endroit où l'on quitte le vocabulaire Dolibarr. Respectez la convention commune, sinon deux modules publieront le même objet sous deux formes.

| Dolibarr | API |
| --- | --- |
| `rowid` | `id` |
| `ref_client` | `customer_ref` |
| `nom` | `name` |
| `town` | `city` |
| `fk_pays` | `country` |
| `fk_departement` | `state` |
| `phone_mobile` | `mobile` |
| `url` | `website` |
| `datec` ou `date_creation` | `created_at` |
| `tms` | `updated_at` |
| `note_public` | `public_note` |
| `note_private` | `private_note` |
| `statut` ou `status` | `status` |

Un mapper qui publie un champ de statut expose aussi `status_label`, le libellé localisé, quand le client le demande explicitement.

> [!WARNING]
> Côté PWA, ne stockez en base locale **que** le nom publié par l'API. Conserver l'alias Dolibarr "au cas où" fabrique une base où la même donnée vit sous deux clés selon son origine. Cas vécu : une liste de tiers affichait "Sans nom" sur toutes ses lignes parce qu'un écran lisait `nom` là où le mapper publie `name`, pendant qu'un autre écran affichait les mêmes tiers correctement.

## Utilisation dans un Controller

```
public function show($payload = null)
{
    global $db;

    $id = (int) $payload['id'];

    $object = new \MyObject($db);
    if ($object->fetch($id) <= 0) {
        dol_syslog(__METHOD__ . ' fetch failed for id=' . $id, LOG_ERR);
        return [['error' => 'not found'], 404];
    }
    $object->fetch_optionals();
    $object->fetch_lines();

    $mapper = new dmMyObject();

    return [$mapper->exportMappedData($object), 200];
}

public function update($payload = null)
{
    global $db, $user;

    $mapper = new dmMyObject();

    try {
        $sanitized = $mapper->importMappedData($payload);
    } catch (\SmartAuth\DolibarrMapping\MapperValidationException $e) {
        dol_syslog(__METHOD__ . ' rejected fields: ' . $e->getMessage(), LOG_WARNING);
        return [['errors' => $e->getErrors()], 400];
    }

    $object = new \MyObject($db);
    if ($object->fetch((int) $payload['id']) <= 0) {
        dol_syslog(__METHOD__ . ' fetch failed', LOG_ERR);
        return [['error' => 'not found'], 404];
    }

    foreach (get_object_vars($sanitized) as $field => $value) {
        $object->$field = $value;
    }

    if ($object->update($user) <= 0) {
        dol_syslog(__METHOD__ . ' update failed: ' . $object->error, LOG_ERR);
        return [['error' => $object->error], 500];
    }

    return [$mapper->exportMappedData($object), 200];
}
```

## Dictionnaires

Un mapper de dictionnaire décrit une ligne d'une table `llx_c_*`. Conventions :
- `protected $type = 'dict';` (le terme `dictionary` est obsolète)
- `$dolibarrClassName` déclaré **si** Dolibarr fournit une classe dédiée (`Ccountry`, `Cstate`, `PaymentTerm`, `CUnits`...), absent sinon
- `$writableFields` reste vide : les dictionnaires se gèrent depuis l'administration Dolibarr
- exposer au minimum `code` et `label`

## Pièges à connaître

| Symptôme | Cause |
| --- | --- |
| `LogicException` au premier `new dmXxx()` | `$dolibarrClassName` manquant, ou classe non chargée par `dol_include_once` |
| `LogicException` mentionnant le parent | `$parentClassName` déclaré sur un mapper d'en-tête, ou égal à `$dolibarrClassName` |
| un champ modifiable est ignoré sans erreur | `$writableFields` contient le nom API au lieu du nom Dolibarr |
| l'objet sort sans ses lignes | `fetch_lines()` non appelé avant l'export |
| les extrafields sont absents | `fetch_optionals()` non appelé, ou `$parentTableElementToUseForExtraFields` incorrect |
| réponse énorme et PWA lente | images en base64 inline dans un champ de liste |
| liste front vide ou "Sans nom" | la PWA lit le nom Dolibarr au lieu du nom API publié |

## Voir aussi
- [Back (PHP)](/back) - Routes et Controllers
- [Requêtes API](/front/requetes-api) - côté React
- [SmartAuth](/smartauth) - socle et mappers du coeur Dolibarr
- [Formation - Mappers](/training/module8-backend-api/mappers)
