# Chapitre 3 : Mappers Les mappers (classes `dm*`) transforment les objets Dolibarr en JSON optimisé pour React. ## Pourquoi des mappers ? Les objets Dolibarr ont des noms de champs souvent cryptiques (`fk_soc`, `rowid`, etc.). Les mappers : - Renomment les champs (rowid → id) - Résolvent les clés étrangères (fk_soc → objet société) - Transforment les données (fichier → base64) - Filtrent les champs exposés ## Structure de base ```php clé React 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(); } } ``` ## Mapping des champs ### Champs simples ```php protected $listOfPublishedFields = [ 'rowid' => 'id', // Renommage 'nom' => 'name', 'address' => 'address', // Même nom 'email' => 'email' ]; ``` ### Clés étrangères Les clés étrangères (fk_*) sont automatiquement résolues : ```php protected $listOfPublishedFields = [ // Résolution automatique 'fk_soc' => 'thirdparty', // ID → objet société 'fk_pays' => 'country', // ID → nom du pays 'fk_user' => 'user', // ID → objet utilisateur 'fk_project' => 'project' // ID → objet projet ]; ``` ### Extrafields ```php protected $listOfPublishedFields = [ // Champs standards 'rowid' => 'id', 'label' => 'label', // Extrafields (préfixe options_) 'options_color' => 'color', 'options_size' => 'size', 'options_myfield' => 'my_custom_field' ]; ``` ## Transformation de valeurs ### Méthodes fieldFilterValueXXX Pour transformer une valeur, créez une méthode `fieldFilterValueXXX` où XXX est le nom du champ : ```php /** * Transformer une date */ public function fieldFilterValueDateCreation($object) { if (empty($object->date_creation)) { return null; } return dol_print_date($object->date_creation, 'dayhour'); } /** * Formater un prix */ public function fieldFilterValuePrice($object) { return (float) $object->price; } /** * Statut en texte */ public function fieldFilterValueStatus($object) { $statuses = [ 0 => 'draft', 1 => 'validated', 2 => 'closed' ]; return $statuses[$object->status] ?? 'unknown'; } ``` ### Logo en base64 ```php 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); } ``` ### Photos attachées ```php 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; } ``` ### Contacts associés ```php 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); } ``` ## Override de types ```php protected $parentFieldsOverride = [ 'duree' => ['type' => 'duration', 'required' => 'required'], 'contacts' => ['type' => 'array'], 'status' => ['type' => 'select'], 'price' => ['type' => 'price'] ]; ``` ## Objets avec lignes Pour les factures, commandes, etc. avec des lignes : ```php 'id', 'ref' => 'ref', 'fk_soc' => 'thirdparty', 'total_ht' => 'total_ht', 'total_ttc' => 'total_ttc', 'fk_statut' => 'status', 'date' => 'date' ]; // Mapping des champs de lignes protected $listOfPublishedFieldsForLines = [ 'id' => 'id', 'desc' => 'description', 'qty' => 'quantity', 'subprice' => 'unit_price', 'total_ht' => 'total_ht', 'total_ttc' => 'total_ttc', 'tva_tx' => 'vat_rate' ]; // Description des champs de lignes (pour le formulaire côté React) protected $parentFieldsForLines = [ 'id' => ['type' => 'integer', 'label' => 'ID', 'visible' => -1], 'desc' => ['type' => 'html', 'label' => 'Description', 'visible' => 1], 'qty' => ['type' => 'integer', 'label' => 'Quantité', 'visible' => 1], 'subprice' => ['type' => 'price', 'label' => 'Prix unitaire', 'visible' => 1] ]; // Titre de la section lignes protected $parentLabelForLines = "Lignes de facture"; public function __construct() { global $langs; $langs->load("bills"); $this->boot(); } } ``` ## Utilisation dans un controller ```php 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]; } // Charger les données additionnelles $invoice->fetch_optionals(); // Extrafields $invoice->fetch_lines(); // Lignes // Mapper $mapper = new dmInvoice(); $data = $mapper->exportMappedData($invoice); return [$data, 200]; } ``` ## Extrafields dynamiques ```php public function __construct() { global $conf, $langs; $langs->load("products"); // Ajouter les extrafields depuis la 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(); } ``` ## Exemple complet ```php '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 en 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); } /** * Statut en texte */ public function fieldFilterValueStatus($object) { return $object->status == 1 ? 'active' : 'inactive'; } } ``` ## Points clés à retenir 1. **listOfPublishedFields** définit le mapping clé Dolibarr → clé React 2. **fieldFilterValueXXX** transforme une valeur spécifique 3. **fetch_optionals()** charge les extrafields 4. **fetch_lines()** charge les lignes pour les objets composés 5. Les clés étrangères (fk_*) sont résolues automatiquement [[:15_training:module8-backend-api:controllers|← Controllers]] | [[:15_training:module8-backend-api:start|Retour au module]] | [[:15_training:module8-backend-api:extrafields|Extrafields →]]