---
source_hash: "3bb44775"
title: "Mapping Dolibarr - React"
weight: 30
---

# Mapping Dolibarr - React

To expose Dolibarr objects to the React application, SmartMaker uses mapping classes prefixed with `dm`. A mapper describes **which fields are published**, **under which name** and **which ones are writable**.

## Before writing a mapper: check that it does not already exist

SmartAuth ships a **library of mappers for the Dolibarr core objects**, in `smartauth/dolMapping/`. Do not rewrite your own for a standard object.

| Domain | Available mappers |
| --- | --- |
| Commercial documents | `dmInvoice`, `dmOrder`, `dmProposal`, `dmContract`, `dmSupplierInvoice`, `dmSupplierOrder`, `dmSupplierProposal`, `dmShipment`, `dmReception`, `dmDeliveryNote` |
| Third parties and contacts | `dmThirdparty`, `dmContact`, `dmSupplier`, `dmUser` |
| Catalogue and stock | `dmProduct`, `dmCategory`, `dmWarehouse`, `dmStockMovement` |
| Production | `dmMo`, `dmBom` |
| Members | `dmMember`, `dmMemberType`, `dmSubscription`, `dmDonation` |
| Project and planning | `dmProject`, `dmTask`, `dmAgendaEvent`, `dmIntervention`, `dmExpenseReport`, `dmTicket` |
| Accounting | `dmBankAccount`, `dmBank`, `dmCompanyBankAccount`, `dmMulticurrency` |
| Dictionaries | `dmC*`: `dmCcountry`, `dmCstate`, `dmCpaymentterm`, `dmCunits`, etc. (read only) |

> [!IMPORTANT]
> A module only writes a mapper for **its own business objects**. For a core object it consumes the SmartAuth mapper, possibly by inheriting from it. A bug fixed in `dolMapping/` benefits every module; a mapper duplicated locally recreates the debt that centralisation removed.

### And often, no mapper at all

For the core objects, SmartAuth exposes a **generic REST facade** built on these mappers. A module then has no CRUD, no search and no pagination to write:

```
GET    objects/{objtype}                paginated list (filters, sorting, search)
GET    objects/{objtype}/describe       field schema (objectDesc)
GET    objects/{objtype}/{id}           one object
POST   objects/{objtype}                creation
PATCH  objects/{objtype}/{id}           update
DELETE objects/{objtype}/{id}           deletion
```

Documents with lines add `objects/{objtype}/{id}/lines`, workflows `objects/{objtype}/{id}/actions/{action}`, invoices `objects/{objtype}/{id}/payments`.

So write a mapper when you have **your own Dolibarr class** to expose. That is the case covered in the rest of this page.

## Declaring a mapper

### Minimal structure

```
<?php
namespace MyModule\Api;

// The target Dolibarr class MUST be loaded here
dol_include_once('/mymodule/class/myobject.class.php');

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

class dmMyObject extends dmBase
{
    use dmTrait;

    // Mapper type: 'object' or 'dict'
    protected $type = 'object';

    // Dolibarr class represented: MANDATORY
    protected $dolibarrClassName = 'MyObject';

    // Element for the extrafields (elementtype column of llx_extrafields)
    protected $parentTableElementToUseForExtraFields = 'myobject';

    // Mapping: Dolibarr name => API name
    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',
    ];

    // Write allowlist: DOLIBARR names, never API names
    protected $writableFields = [
        'label',
        'description',
        'note_public',
    ];

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

Calling `boot()` at the end of the constructor is mandatory.

### Recognised properties

| Property | Mandatory | Role |
| --- | --- | --- |
| `$type` | yes | `object` or `dict` |
| `$dolibarrClassName` | yes for `object` | exact name of the Dolibarr class represented |
| `$listOfPublishedFields` | yes | map of Dolibarr name to API name |
| `$writableFields` | no, defaults to `[]` | write allowlist, in Dolibarr names |
| `$listOfDerivedFields` | no | computed fields with no source column |
| `$parentTableElementToUseForExtraFields` | no | extrafields attachment |
| `$parentClassName` | no | **only** for a sub-object or line mapper |
| `$parentClassNameForLines` | no | Dolibarr class of the lines |
| `$listOfPublishedFieldsForLines` | no | map of the line fields |
| `$parentLabelForLines` | no | API key under which the lines are published |
| `$parentFieldsOverride` | no | patch of a field definition (type, required...) |

### Pitfall: $dolibarrClassName is not derived from the mapper name

> [!WARNING]
> `$dolibarrClassName` is **mandatory** on every mapper of type `object`. It is not derived from the class name, and for good reason: the derivation would be wrong in most cases.

| Mapper | Actual Dolibarr class | What a derivation would give |
| --- | --- | --- |
| `dmThirdparty` | `Societe` | `Thirdparty` |
| `dmInvoice` | `Facture` | `Invoice` |
| `dmOrder` | `Commande` | `Order` |
| `dmProposal` | `Propal` | `Proposal` |
| `dmIntervention` | `Fichinter` | `Intervention` |
| `dmWarehouse` | `Entrepot` | `Warehouse` |
| `dmMember` | `Adherent` | `Member` |
| `dmShipment` | `Expedition` | `Shipment` |

The mapper validates its declaration at `boot()` and raises an explicit `LogicException` if `$dolibarrClassName` is missing, or if the announced class does not exist (typically, a forgotten `dol_include_once`).

### Pitfall: never use $parentClassName on a top-level object

`$parentClassName` is **only** for a sub-object or line mapper, to reach back to its parent. A header mapper must not declare it.

```
// WRONG: Product has no parent, and the property is the wrong one
class dmProduct extends dmBase
{
    use dmTrait;
    protected $parentClassName = 'Product';
}

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

// CORRECT: line mapper, with a real parent
class dmFichinterLigne extends dmBase
{
    use dmTrait;
    protected $type = 'object';
    protected $dolibarrClassName = 'FichinterLigne';
    protected $parentClassName   = 'Fichinter';
}
```

Remember the distinction in one sentence: `$dolibarrClassName` answers "who am I?", `$parentClassName` answers "who is my parent?". Declaring both identical raises a `LogicException` at boot.

## Reading: exportMappedData()

`exportMappedData()` converts a Dolibarr object into a JSON object using the API field names.

```
$object = new \MyObject($db);
$object->fetch($id);
$object->fetch_optionals();  // extrafields
$object->fetch_lines();      // lines, if the object has any

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

What the method does along the way:
- it resolves the foreign keys declared in the Dolibarr `$fields`, over two levels of depth at most
- it includes the extrafields declared as `options_xxx`
- it includes the associated categories when the object has some (product, third party, contact, member)
- it exposes `nb_linked_files`, and the full file list if `$mapper->withFiles = true`

### Describing the schema to the front

`objectDesc()` returns the structural description of the object: fields, API types, translated labels, display position. This is what lets the front generate forms and lists without hard-coding the fields. The result is computed once at boot and cached.

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

## Writing: writableFields and importMappedData()

`importMappedData()` is the inverse of `exportMappedData()`: it takes an API payload and returns a `stdClass` with Dolibarr names, ready to be applied to the object.

```
$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);
```

### The contract

  - any field absent from `$writableFields` is **rejected**, not ignored
  - all the rejections are collected and reported in a single exception, not on the first one met
  - API names are automatically mapped back to Dolibarr names
  - each value is cast according to the type declared in the Dolibarr `$fields`: `integer` to `int`, `price` or `double` to `float`, `bool*` to 0 or 1, `date` to a timestamp
  - the `lines` key always raises an exception: lines do not go through `importMappedData()`

A mapper that does not declare `$writableFields` is **entirely read only**. That is the default behaviour, and it is deliberate.

### Pitfall: writableFields holds Dolibarr names

> [!WARNING]
> Each entry of `$writableFields` must be a **key** of `$listOfPublishedFields` (the Dolibarr name), never a value (the API name). This is a silent bug: the field is rejected, no error is visible on the client side, and the object is never updated.

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

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

// WRONG: 'name' is the API name, not the Dolibarr name
protected $writableFields = ['name'];
```

This pitfall was hit three times in production before being locked down. The boot now raises a `LogicException` listing the offending entries.

### What importMappedData() does not do
- no business validation: the mapper sanitises and casts, Dolibarr validates on persistence
- no line writing: go through `addline()` and `updateline()`
- best-effort casting: a string `"abc"` in an integer field becomes `0`, with no warning

## Transforming a value: fieldFilterValueXxx()

To transform a field before export, declare a public method `fieldFilterValue` followed by the Dolibarr name of the field in CamelCase. The method name **is** the contract: no annotation, no registration.

```
/**
 * Returns a signed URL instead of the raw file name.
 */
public function fieldFilterValueLogo($object, $value)
{
    return '/upload/societe/' . $object->id . '/' . urlencode($value);
}

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

Typical use cases: converting a timestamp, translating a code into a label, computing a value derived from another field.

### Derived fields without a Dolibarr column

To publish a key that is not backed by any column, declare it in `$listOfDerivedFields` and not in `$listOfPublishedFields`. The `fieldFilterValueXxx()` method is then called without first checking that the source field is present.

> [!WARNING]
> Do not return base64 images in a list field. A list of 200 third parties with their logo inline saturates the response and the local database of the PWA. The convention is to publish a **media URL** and, for lists, a `logo_mini` thumbnail.

## Extrafields

Extrafields are published like ordinary fields, by prefixing their name with `options_`:

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

The attachment is done through `$parentTableElementToUseForExtraFields`, which must be exactly the `elementtype` column of `llx_extrafields` for that object.

### Extrafields configurable by the administrator

The SmartBoot skeleton shows the pattern: two constants list the extrafields to expose, read only and read-write, and the constructor adds them to the 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]
> The extrafields themselves are never created in SQL. They are declared through `$extrafields->addExtraField(...)` in the `init()` of the module descriptor.

## Objects with lines

```
// Dolibarr class of the lines
protected $parentClassNameForLines = 'MyObjectLine';

// Description of the line fields, to generate the form
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 of the line fields
protected $listOfPublishedFieldsForLines = [
    'id'       => 'id',
    'date'     => 'date',
    'desc'     => 'description',
    'qty'      => 'quantity',
    'subprice' => 'unit_price',
    'total_ht' => 'total',
];

// API key under which the lines are published
protected $parentLabelForLines = "linesDetail";
```

Lines are exposed for reading through this mechanism. For writing they go through the native Dolibarr methods `addline()`, `updateline()` and `deleteline()`, or through the facade routes `objects/{objtype}/{id}/lines` for the core objects.

> [!NOTE]
> `fetch()` does not always load the lines. Call `fetch_lines()` before the export, otherwise the object comes out without its lines.

## Adjusting a field description

`$parentFieldsOverride` patches the definition of a field coming up from Dolibarr, without touching the upstream class.

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

Typically: rendering a duration stored in seconds as a `duration` field on the front side, or making mandatory a field that Dolibarr considers optional.

## Naming convention for the API fields

The mapper is where the Dolibarr vocabulary is left behind. Follow the common convention, otherwise two modules will publish the same object in two different shapes.

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

A mapper that publishes a status field also exposes `status_label`, the localised label, when the client explicitly asks for it.

> [!WARNING]
> On the PWA side, store in the local database **only** the name published by the API. Keeping the Dolibarr alias "just in case" builds a database where the same data lives under two keys depending on where it came from. Real case: a third-party list displayed "Unnamed" on every row because one screen read `nom` where the mapper publishes `name`, while another screen displayed the same third parties correctly.

## Usage in a 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];
}
```

## Dictionaries

A dictionary mapper describes one row of a `llx_c_*` table. Conventions:
- `protected $type = 'dict';` (the term `dictionary` is obsolete)
- `$dolibarrClassName` declared **if** Dolibarr provides a dedicated class (`Ccountry`, `Cstate`, `PaymentTerm`, `CUnits`...), absent otherwise
- `$writableFields` stays empty: dictionaries are managed from the Dolibarr administration
- expose at least `code` and `label`

## Pitfalls to know about

| Symptom | Cause |
| --- | --- |
| `LogicException` on the first `new dmXxx()` | `$dolibarrClassName` missing, or class not loaded by `dol_include_once` |
| `LogicException` mentioning the parent | `$parentClassName` declared on a header mapper, or equal to `$dolibarrClassName` |
| a writable field is ignored with no error | `$writableFields` holds the API name instead of the Dolibarr name |
| the object comes out without its lines | `fetch_lines()` not called before the export |
| the extrafields are missing | `fetch_optionals()` not called, or `$parentTableElementToUseForExtraFields` incorrect |
| huge response and slow PWA | base64 images inline in a list field |
| empty front list or "Unnamed" | the PWA reads the Dolibarr name instead of the published API name |

## See also
- [Back (PHP)](/back) - Routes and Controllers
- [API requests](/front/requetes-api) - React side
- [SmartAuth](/smartauth) - foundation and mappers of the Dolibarr core
- [Training - Mappers](/training/module8-backend-api/mappers)
