Chapter 3: Mappers

Mapper classes (dm*) transform Dolibarr objects into optimized JSON for React.

Why Mappers?

Dolibarr objects often have cryptic field names (fk_soc, rowid, etc.). Mappers:

  • Rename fields (rowid -> id)
  • Resolve foreign keys (fk_soc -> thirdparty object)
  • Transform data (file -> base64)
  • Filter exposed fields

Basic Structure

<?php
// smartmaker-api/Mappers/dmProduct.php

namespace MonModule\Api\Mappers;

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

    // Object type
    protected $type = "object";

    // Source Dolibarr class
    protected $parentClassName = 'Product';

    // For extrafields
    protected $parentClassToUseForExtraFields = "Product";
    protected $parentElementToUseForExtraFields = "product";
    protected $parentTableElementToUseForExtraFields = 'product';

    // Mapping: Dolibarr key => React key
    protected $listOfPublishedFields = [
        'rowid'         => 'id',
        'ref'           => 'ref',
        'label'         => 'label',
        'description'   => 'description',
        'price'         => 'price',
        'price_ttc'     => 'price_ttc',
        'tva_tx'        => 'vat_rate',
        'stock_reel'    => 'stock',
        'tosell'        => 'status',
        'date_creation' => 'created_at',
        'tms'           => 'updated_at'
    ];

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

Field Mapping

Simple Fields

protected $listOfPublishedFields = [
    'rowid'     => 'id',           // Rename
    'nom'       => 'name',
    'address'   => 'address',      // Same name
    'email'     => 'email'
];

Foreign Keys

Foreign keys (fk_*) are automatically resolved:

protected $listOfPublishedFields = [
    // Automatic resolution
    'fk_soc'        => 'thirdparty',   // ID -> thirdparty object
    'fk_pays'       => 'country',       // ID -> country name
    'fk_user'       => 'user',          // ID -> user object
    'fk_project'    => 'project'        // ID -> project object
];

Extrafields

protected $listOfPublishedFields = [
    // Standard fields
    'rowid' => 'id',
    'label' => 'label',

    // Extrafields (options_ prefix)
    'options_color'     => 'color',
    'options_size'      => 'size',
    'options_myfield'   => 'my_custom_field'
];

Value Transformation

fieldFilterValueXXX Methods

To transform a value, create a fieldFilterValueXXX method where XXX is the field name:

/**
 * Transform a date
 */
public function fieldFilterValueDateCreation($object)
{
    if (empty($object->date_creation)) {
        return null;
    }
    return dol_print_date($object->date_creation, 'dayhour');
}

/**
 * Format a price
 */
public function fieldFilterValuePrice($object)
{
    return (float) $object->price;
}

/**
 * Status as text
 */
public function fieldFilterValueStatus($object)
{
    $statuses = [
        0 => 'draft',
        1 => 'validated',
        2 => 'closed'
    ];
    return $statuses[$object->status] ?? 'unknown';
}

Logo as base64

public function fieldFilterValueLogo($object)
{
    global $conf;

    $dir = $conf->societe->multidir_output[$object->entity];
    $logoPath = $dir . "/" . $object->id . "/logos/" . $object->logo;

    if (!file_exists($logoPath)) {
        return null;
    }

    $type = pathinfo($logoPath, PATHINFO_EXTENSION);
    $content = file_get_contents($logoPath);

    return 'data:image/' . $type . ';base64,' . base64_encode($content);
}

Attached Photos

public function fieldFilterValuePhotos($object)
{
    global $conf;

    $photos = [];
    $dir = $conf->product->dir_output . '/' . $object->ref;

    if (!is_dir($dir)) {
        return $photos;
    }

    $files = scandir($dir);
    foreach ($files as $file) {
        if (preg_match('/\.(jpg|jpeg|png|gif)$/i', $file)) {
            $path = $dir . '/' . $file;
            $type = pathinfo($path, PATHINFO_EXTENSION);
            $content = file_get_contents($path);

            $photos[] = [
                'name' => $file,
                'src' => 'data:image/' . $type . ';base64,' . base64_encode($content)
            ];
        }
    }

    return $photos;
}

Associated Contacts

public function fieldFilterValueContacts($object)
{
    $contacts = $object->liste_contact(-1, 'external');

    return array_map(function($c) {
        return [
            'id' => $c['id'],
            'name' => $c['firstname'] . ' ' . $c['lastname'],
            'email' => $c['email'],
            'phone' => $c['phone']
        ];
    }, $contacts);
}

Type Override

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

Objects with Lines

For invoices, orders, etc. with lines:

<?php
namespace MonModule\Api\Mappers;

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

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

    // Class for lines
    protected $parentClassNameForLines = 'FactureLigne';

    // Mapping of main fields
    protected $listOfPublishedFields = [
        'rowid'       => 'id',
        'ref'         => 'ref',
        'fk_soc'      => 'thirdparty',
        'total_ht'    => 'total_ht',
        'total_ttc'   => 'total_ttc',
        'fk_statut'   => 'status',
        'date'        => 'date'
    ];

    // Mapping of line fields
    protected $listOfPublishedFieldsForLines = [
        'id'          => 'id',
        'desc'        => 'description',
        'qty'         => 'quantity',
        'subprice'    => 'unit_price',
        'total_ht'    => 'total_ht',
        'total_ttc'   => 'total_ttc',
        'tva_tx'      => 'vat_rate'
    ];

    // Description of line fields (for React form)
    protected $parentFieldsForLines = [
        'id'       => ['type' => 'integer', 'label' => 'ID', 'visible' => -1],
        'desc'     => ['type' => 'html', 'label' => 'Description', 'visible' => 1],
        'qty'      => ['type' => 'integer', 'label' => 'Quantity', 'visible' => 1],
        'subprice' => ['type' => 'price', 'label' => 'Unit Price', 'visible' => 1]
    ];

    // Section title for lines
    protected $parentLabelForLines = "Invoice Lines";

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

Usage in a Controller

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

    $id = $payload['id'];

    require_once DOL_DOCUMENT_ROOT . '/compta/facture/class/facture.class.php';

    $invoice = new \Facture($db);
    $res = $invoice->fetch($id);

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

    // Load additional data
    $invoice->fetch_optionals();  // Extrafields
    $invoice->fetch_lines();      // Lines

    // Map
    $mapper = new dmInvoice();
    $data = $mapper->exportMappedData($invoice);

    return [$data, 200];
}

Dynamic Extrafields

public function __construct()
{
    global $conf, $langs;

    $langs->load("products");

    // Add extrafields from configuration
    $extrafields = getDolGlobalString('MYMODULE_PRODUCT_EXTRAFIELDS');

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

    $this->boot();
}

Complete Example

<?php
// smartmaker-api/Mappers/dmThirdparty.php

namespace MonModule\Api\Mappers;

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

    protected $type = "object";
    protected $parentClassName = 'Societe';
    protected $parentClassToUseForExtraFields = "Societe";
    protected $parentElementToUseForExtraFields = "societe";
    protected $parentTableElementToUseForExtraFields = 'societe';

    protected $listOfPublishedFields = [
        'rowid'         => 'id',
        'nom'           => 'name',
        'name_alias'    => 'alias',
        'address'       => 'address',
        'zip'           => 'zip',
        'town'          => 'city',
        'fk_pays'       => 'country',
        'phone'         => 'phone',
        'email'         => 'email',
        'url'           => 'website',
        'siren'         => 'siren',
        'siret'         => 'siret',
        'tva_intra'     => 'vat_number',
        'logo'          => 'logo',
        'status'        => 'status',
        'date_creation' => 'created_at',

        // Extrafields
        'options_category' => 'category'
    ];

    protected $parentFieldsOverride = [
        'logo' => ['type' => 'image']
    ];

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

    /**
     * Logo as base64
     */
    public function fieldFilterValueLogo($object)
    {
        global $conf;

        if (empty($object->logo)) {
            return null;
        }

        $dir = $conf->societe->multidir_output[$object->entity];
        $path = $dir . "/" . $object->id . "/logos/" . $object->logo;

        if (!file_exists($path)) {
            return null;
        }

        $ext = pathinfo($path, PATHINFO_EXTENSION);
        $content = file_get_contents($path);

        return 'data:image/' . $ext . ';base64,' . base64_encode($content);
    }

    /**
     * Status as text
     */
    public function fieldFilterValueStatus($object)
    {
        return $object->status == 1 ? 'active' : 'inactive';
    }
}

Key Points to Remember

  1. listOfPublishedFields defines the Dolibarr key -> React key mapping
  2. fieldFilterValueXXX transforms a specific value
  3. fetch_optionals() loads extrafields
  4. fetch_lines() loads lines for compound objects
  5. Foreign keys (fk_*) are automatically resolved

Previous Chapter: Controllers | Back to Module | Next: Extrafields ->