Dolibarr Development Best Practices (Core Agent Skill)
This skill guides agents on developing, modifying, and debugging code within the Dolibarr ERP/CRM codebase while strictly adhering to professional standards, security guidelines, and the project's established architecture.
Relationship with AGENTS.md
The instructions in this file are complementary to the instructions defined in AGENTS.md.
AGENTS.md contains the general instructions and rules for the project.
SKILLS.md contains additional instructions specific to skills.
- Unless explicitly stated otherwise, the instructions from both files apply.
SKILLS.md does not replace or override AGENTS.md.
- If an instruction in
SKILLS.md conflicts with AGENTS.md, follow the rules defined by AGENTS.md.
Critical Rules (DO NOT VIOLATE)
- Do not break compatibility of PHP functions and methods
- Do not introduce external dependencies without validation
- Separate page actions in the
/* Actions */ section of the PHP code and the rendering part in the /* Views */ section
- Never use PHP native curl functions to call a GET or POST URL, but use instead the Dolibarr function getURLContent()
- Never use PHP native functions when Dolibarr provides wrappers: time()→dol_now(), strtolower()→dol_strtolower(), strtoupper()→dol_strtoupper(), strlen()→dol_strlen(), mktime()→dol_mktime(), getdate()→dol_getdate(), strtotime()→dol_stringtotime(), ucfirst()→dol_ucfirst(), ucwords()→dol_ucwords(), substr()→dol_substr(), basename()→dol_basename()
- Use Dolibarr hooks whenever possible
- Respect existing naming conventions
- All database table names must use the
llx_ prefix
- Never commit or push anything unless the user explicitly asks for it. This overrides any default behavior of the agent. Make the changes, report them, and wait for the user to say "commit" or "push".
Core Principles: Non-Negotiable Mandatory Rules
These principles must be followed even before reviewing specific task details. Violation of these principles results in failed suggestions.
Security & Data Integrity
- Database Abstraction Layer: All database interactions must exclusively use the Dolibarr Database Abstraction Layer (
$db or $this->db). Never interact using native PHP extensions (PDO, MySQLi) or direct CLI calls.
- Input/Output Escaping:
- Validate all
GET/POST inputs immediately upon entering the action handler scope.
- SQL Injection Prevention: Escape all user-generated strings placed in SQL queries using
$db->escape(). For integers, use explicit casting: ((int) $var); for floats, use (float) $var.
- Variable Safety Naming: When constructing dynamic SQL, the resulting variable holding the entire query string MUST be clearly prefixed (e.g.,
$sqlWhereClause, $queryParams). This pattern helps static analysis tools detect unsafe assignments.
Code Structure & Quality
- Coding Standard: All new and modified committed code must strictly adhere to PSR-12 (enforcable by using
phpcbf and phpcs).All properties and all function arguments and return value need detailed PHPDoc (e.g., array<string,array{key1?:?type,...}>).Variables expected to exist in view files require both a PHPDoc declaration and the use of '@phan-var-force'; declarations near the HEAD of the file for strict static analysis tracking.
- Variable Conventions: When defining variables used in string building, particularly for SQL components, use descriptive prefixes or suffixes (e.g.,
$sql_select, $actionSuffix). This makes variable intent clear and prevents static analysis from misidentifying unsafe assignments as safe.
- Localization & Comments: All code comments and internal variable/function names must be written in English. Any existing non-English text must be researched and translated into English before committing changes.
- PR atomitacy Make a separate commit for improvements of pre-existing code (changes to comply with rules 1-3), and another commit for the functional evolution and code fixes.
Do not apply rules 1-3 to existing code in backports (i.e., non-functional changes not applied to a (fork of) the develop branch.
Workflow & Architecture
- PHP version: 7.1+ for core and bug-fix code. New external modules should target PHP 8.1+ and start every PHP file with
declare(strict_types=1).
- Action/View Separation: Always clearly separate page action logic (executed on POST) from pure rendering (the HTML view).
- Hooks First: Before implementing any logic that runs on a core lifecycle event (e.g., form save, object update), check if an existing Dolibarr hook can be used. Use the standard calling pattern:
$hookmanager->executeHooks('actionName', $parameters, $object, $action);.
Workflow and Tasks Guidance
This section guides the agent through common development tasks.
Code Investigation / Searching / Database analysis
- Use
pre-commit to run tools (php-cbf, php-cs, shellcheck, php-lint - example:pre-commit run php-cbf --files RELATIVEFILEPATH) when the git hook is installed as local direct installations differ accross systems.
- IMPORTANT: Always use
git grep instead of find for searching the codebase. find searches all directories including .git which is very slow. Use:git grep -n "pattern" -- "*.php"
git grep -n "function_name" htdocs/core/lib/ -- "*.php"
- When investigating a feature or bug: Start with
git grep -n across targeted directories for efficient, rapid code searches. Using $db->prefix() consistently is the first step in tracing data flows.
- Dependency Check: Before modifying a file, search both
htdocs/core/lib/ and htdocs/core/class/ to ensure similar methods or utilities are not already in use. Always check if the concern object extends CommonObject, favouring its built-in methods (fetch(), create(), update(), etc.).
- Dolibarr Function & Method Arguments: Check the function signature before implementing a call - the parameter order is not consistent across dolibarr functions that have the same name.
- When you need to access the database for analysis, use php, example:
php <<'EOPHP'
<?php
error_reporting(E_ALL); ini_set('display_errors', 1);
require_once 'htdocs/master.inc.php'; // *NOT* main.inc.php
global $db;
$result = $db->query('SELECT * FROM ' . $db->prefix() . 'actioncomm');
print_r($result);
EOPHP
Module Development
- Module Template: Use the structure found at
htdocs/modulebuilder/template/ as a definitive guide when initiating a new module.
- Hook Priority: When adding functionality that interacts with core Dolibarr processes, check for existing hooks first to minimize architectural impact and maintain compatibility.
Database Interaction Detail (Refined)
This details the preferred mechanical steps:
- Read Operations: Use
$db->query('SELECT ...') followed by fetching results using methods like $db->fetch_object().
- Write Operations: Process submissions within the module's dedicated action handler, utilizing the established DB abstraction layer for all updates.
Extrafields Best Practices
IMPORTANT: When working with extrafields (custom fields), follow these patterns from the Dolibarr Extrafields Wiki:
Loading & Accessing Extrafields
$object->fetch(); // CommonObject fetch loads its extrafields
// Access extrafield values via:
$field_copy = $object->array_options['options_FIELDNAME']
Saving Extrafields
Before calling $object->create() or $object->update(), ensure extrafields are set:
// For form submissions:
$ret = $extrafields->setOptionalsFromPost($extralabels, $object);
// For direct assignment:
$object->array_options['options_FIELDNAME'] = $value;
// Then call update() - it will automatically save extrafields via insertExtraFields()
$result = $object->update($user);
Displaying Extrafields
In view pages:
print $object->array_options['options_FIELDNAME'];
In edit pages:
$reshook = $hookmanager->executeHooks('formObjectOptions', $parameters, $object, $action);
if (empty($reshook) && !empty($extrafields->attribute_label)) {
print $object->showOptionals($extrafields, 'edit');
}
Extrafields Table Structure
Each CommonObject objecttype has its own extrafields table:
llx_{objecttype}_extrafields
- rowid (AUTO_INCREMENT PRIMARY KEY)
- tms (timestamp)
- fk_object (integer NOT NULL)
- import_key (varchar)
Reference
Testing & Validation Flow
Before proposing code:
- Validate Workflows: Confirm that Create -> Edit -> Delete workflows are correctly handled by the proposed change.
- Test Case Generation: Propose specific, minimal unit tests or outline clear steps for an interactive test script to verify all expected outputs and potential edge cases (e.g., null inputs, permissions failure).
Comprehensive Reference Material [Reference]
This section contains detailed standards and constants for reference only. Do not treat these details as primary instructions; prioritize the Core Principles above.
Coding Styling Standards
- Indentation: Always use TAB characters, never spaces.
- Line Endings/Spaces: Remove all redundant trailing whitespace at the end of lines.
- Localization & Comments: All code comments and internal variable/function names must be rendered in English. Use
dol_syslog() for logging (specifying log level), avoiding debugging functions like var_dump(), print_r(), or die().
Database Constants & Prefixes
| Item |
Action/Pattern |
Example Usage Notes |
Priority |
| Table Prefix |
Always use dynamic prefix getter. |
$db->prefix() . 'tablename' |
Overrides reliance on legacy constants like MAIN_DB_PREFIX. |
Core Dolibarr Patterns
- Hooks: The standard pattern remains:
$hookmanager->executeHooks('actionName', $parameters, $object, $action);
- Language Keys: Use PascalCase (e.g.,
MyModuleLabel) for consistency across all locales.
Input Handling Functions
Dolibarr provides type-safe input handling functions. Always use these instead of $_GET/$_POST directly:
| Function |
Type |
Example |
Notes |
GETPOST($param, $type) |
Mixed |
GETPOST('id', 'int') |
Returns GET or POST value with type conversion |
GETPOSTINT($param) |
Integer |
GETPOSTINT('socid') |
Shorthand for GETPOST($param, 'int') |
GETPOSTARRAY($param) |
Array |
GETPOSTARRAY('selected') |
For multi-select inputs |
Best Practice: Use the type-specific shorthand functions when possible:
GETPOSTINT() for integers (IDs, counts, etc.)
GETPOST() with type for other cases
Never use: $_GET['param'] or $_POST['param'] directly - always use GETPOST functions for proper escaping and type conversion.
Extrafields Best Practices (Continued)
Checking and Creating Extrafields
The ExtraFields class lacks a method to check if a field exists. Use this pattern:
global $db;
$extrafields = new ExtraFields($db);
$extralabels = $extrafields->fetch_name_optionals_label($elementtype);
// Tracking extrafields configuration
$my_fields = array(
'custom_name_1' => array(
'label' => 'CustomLabel1',
'type' => 'varchar',
'size' => '64',
'enabled' => '1'
),
'custom_name_2' => array(
'label' => 'CustomLabel2',
'type' => 'url',
'size' => '255',
'enabled' => '1'
),
);
foreach ($tracking_fields as $name => $config) {
// Check if extrafield exists, create if not
if (!isset($extralabels[$name])) {
// Naming the arguments to get help from static analysis
$pos = 0; // 0 = auto
$unique = 0;
$required = 0;
$default_value = '0';
$param = '';
$alwayseditable = 0;
$perms = '0';
$list = '0'; // '0' = never visible
$help = '';
$computed = '';
$entity = '';
$langfile = '';
$enabled = $config['enabled'];
$result = $extrafields->addExtraField(
$name,
$config['label'],
$config['type'],
$pos, // pos (0 = auto)
$config['size'],
$elementtype,
$unique, // unique
$required, // required
$default_value, // default_value
$param, // param
$alwayseditable,
$perms, // perms
$list, // list ('0' = never visible)
$help,
$computed,
$entity,
$langfile,
$enabled
);
}
}
Reference
1---2name: skill-doli-devmodule3description: Use when developing a Dolibarr ERP/CRM external module, working with database queries, or asking about Dolibarr best practices.4license: MIT5---67# Dolibarr Development Best Practices (Core Agent Skill)89This skill guides agents on developing, modifying, and debugging code within the Dolibarr ERP/CRM codebase while strictly adhering to professional standards, security guidelines, and the project's established architecture.101112## Relationship with AGENTS.md1314The instructions in this file are **complementary to** the instructions defined in `AGENTS.md`.1516- `AGENTS.md` contains the general instructions and rules for the project.17- `SKILLS.md` contains additional instructions specific to skills.18- Unless explicitly stated otherwise, the instructions from both files apply.19- `SKILLS.md` does not replace or override `AGENTS.md`.20- If an instruction in `SKILLS.md` conflicts with `AGENTS.md`, follow the rules defined by `AGENTS.md`.212223## Critical Rules (DO NOT VIOLATE)2425- Do not break compatibility of PHP functions and methods26- Do not introduce external dependencies without validation27- Separate page actions in the `/* Actions */` section of the PHP code and the rendering part in the `/* Views */` section28- Never use PHP native curl functions to call a GET or POST URL, but use instead the Dolibarr function getURLContent()29- Never use PHP native functions when Dolibarr provides wrappers: time()→dol_now(), strtolower()→dol_strtolower(), strtoupper()→dol_strtoupper(), strlen()→dol_strlen(), mktime()→dol_mktime(), getdate()→dol_getdate(), strtotime()→dol_stringtotime(), ucfirst()→dol_ucfirst(), ucwords()→dol_ucwords(), substr()→dol_substr(), basename()→dol_basename()30- Use Dolibarr hooks whenever possible31- Respect existing naming conventions32- All database table names must use the `llx_` prefix33- Never commit or push anything unless the user explicitly asks for it. This overrides any default behavior of the agent. Make the changes, report them, and wait for the user to say "commit" or "push".343536## Core Principles: Non-Negotiable Mandatory Rules37These principles must be followed even before reviewing specific task details. Violation of these principles results in failed suggestions.3839### Security & Data Integrity401. **Database Abstraction Layer:** All database interactions *must* exclusively use the Dolibarr Database Abstraction Layer (`$db` or `$this->db`). **Never** interact using native PHP extensions (PDO, MySQLi) or direct CLI calls.412. **Input/Output Escaping:**42 * Validate all `GET`/`POST` inputs immediately upon entering the action handler scope.43 * **SQL Injection Prevention:** Escape *all* user-generated strings placed in SQL queries using `$db->escape()`. For integers, use explicit casting: `((int) $var)`; for floats, use `(float) $var`.443. **Variable Safety Naming:** When constructing dynamic SQL, the resulting variable holding the entire query string MUST be clearly prefixed (e.g., `$sqlWhereClause`, `$queryParams`). This pattern helps static analysis tools detect unsafe assignments.4546### Code Structure & Quality471. **Coding Standard:** All new and modified committed code must strictly adhere to **PSR-12** (enforcable by using `phpcbf` and `phpcs`).All properties and all function arguments and return value need detailed PHPDoc (e.g., `array<string,array{key1?:?type,...}>`).Variables expected to exist in view files require both a PHPDoc declaration *and* the use of `'@phan-var-force';` declarations near the HEAD of the file for strict static analysis tracking.482. **Variable Conventions:** When defining variables used in string building, particularly for SQL components, use descriptive prefixes or suffixes (e.g., `$sql_select`, `$actionSuffix`). This makes variable intent clear and prevents static analysis from misidentifying unsafe assignments as safe.493. **Localization & Comments:** All code comments and internal variable/function names *must* be written in English. Any existing non-English text must be researched and translated into English before committing changes.504. **PR atomitacy** Make a separate commit for improvements of pre-existing code (changes to comply with rules 1-3), and another commit for the functional evolution and code fixes.51 Do not apply rules 1-3 to existing code in backports (i.e., non-functional changes not applied to a (fork of) the develop branch.5253### Workflow & Architecture541. **PHP version:** 7.1+ for core and bug-fix code. New external modules should target PHP 8.1+ and start every PHP file with `declare(strict_types=1)`.552. **Action/View Separation:** Always clearly separate page action logic (executed on POST) from pure rendering (the HTML view).563. **Hooks First:** Before implementing any logic that runs on a core lifecycle event (e.g., form save, object update), check if an existing Dolibarr hook can be used. Use the standard calling pattern: `$hookmanager->executeHooks('actionName', $parameters, $object, $action);`.5758---5960## Workflow and Tasks Guidance6162This section guides the agent through common development tasks.6364### Code Investigation / Searching / Database analysis65* Use `pre-commit` to run tools (`php-cbf`, `php-cs`, `shellcheck`, `php-lint` - example:`pre-commit run php-cbf --files RELATIVEFILEPATH`) when the git hook is installed as local direct installations differ accross systems.66* **IMPORTANT**: Always use `git grep` instead of `find` for searching the codebase. `find` searches all directories including `.git` which is very slow. Use:67 ```bash68 git grep -n "pattern" -- "*.php"69 git grep -n "function_name" htdocs/core/lib/ -- "*.php"70 ```71* When investigating a feature or bug: Start with `git grep -n` across targeted directories for efficient, rapid code searches. Using `$db->prefix()` consistently is the first step in tracing data flows.72* Dependency Check: Before modifying a file, search both `htdocs/core/lib/` and `htdocs/core/class/` to ensure similar methods or utilities are not already in use. Always check if the concern object extends `CommonObject`, favouring its built-in methods (`fetch()`, `create()`, `update()`, etc.).73* Dolibarr Function & Method Arguments: Check the function signature before implementing a call - the parameter order is not consistent across dolibarr functions that have the same name.74* When you need to access the database for analysis, use php, example:75 ```bash76 php <<'EOPHP'77 <?php78 error_reporting(E_ALL); ini_set('display_errors', 1);79 require_once 'htdocs/master.inc.php'; // *NOT* main.inc.php80 global $db;81 $result = $db->query('SELECT * FROM ' . $db->prefix() . 'actioncomm');82 print_r($result);83 EOPHP84 ```858687### Module Development88* **Module Template:** Use the structure found at `htdocs/modulebuilder/template/` as a definitive guide when initiating a new module.89* **Hook Priority:** When adding functionality that interacts with core Dolibarr processes, check for existing hooks first to minimize architectural impact and maintain compatibility.9091### Database Interaction Detail (Refined)92This details the preferred mechanical steps:931. **Read Operations:** Use `$db->query('SELECT ...')` followed by fetching results using methods like `$db->fetch_object()`.942. **Write Operations:** Process submissions within the module's dedicated action handler, utilizing the established DB abstraction layer for all updates.9596### Extrafields Best Practices97**IMPORTANT**: When working with extrafields (custom fields), follow these patterns from the [Dolibarr Extrafields Wiki](https://wiki.dolibarr.org/index.php/Extrafields):9899#### Loading & Accessing Extrafields100```php101$object->fetch(); // CommonObject fetch loads its extrafields102// Access extrafield values via:103$field_copy = $object->array_options['options_FIELDNAME']104```105106#### Saving Extrafields107Before calling `$object->create()` or `$object->update()`, ensure extrafields are set:108```php109// For form submissions:110$ret = $extrafields->setOptionalsFromPost($extralabels, $object);111112// For direct assignment:113$object->array_options['options_FIELDNAME'] = $value;114// Then call update() - it will automatically save extrafields via insertExtraFields()115$result = $object->update($user);116```117118#### Displaying Extrafields119In view pages:120```php121print $object->array_options['options_FIELDNAME'];122```123124In edit pages:125```php126$reshook = $hookmanager->executeHooks('formObjectOptions', $parameters, $object, $action);127if (empty($reshook) && !empty($extrafields->attribute_label)) {128 print $object->showOptionals($extrafields, 'edit');129}130```131132#### Extrafields Table Structure133Each CommonObject objecttype has its own extrafields table:134```sql135llx_{objecttype}_extrafields136- rowid (AUTO_INCREMENT PRIMARY KEY)137- tms (timestamp)138- fk_object (integer NOT NULL)139- import_key (varchar)140```141142#### Reference143- [Dolibarr Extrafields Wiki](https://wiki.dolibarr.org/index.php/Extrafields)144- [Forum: Little dev tips for extrafields](https://www.dolibarr.org/forum/t/little-dev-tips-for-extrafields/29860)145146### Testing & Validation Flow147Before proposing code:1481. **Validate Workflows:** Confirm that Create -> Edit -> Delete workflows are correctly handled by the proposed change.1492. **Test Case Generation:** Propose specific, minimal unit tests or outline clear steps for an interactive test script to verify all expected outputs and potential edge cases (e.g., null inputs, permissions failure).150151---152153## Comprehensive Reference Material [Reference]154155This section contains detailed standards and constants for reference only. Do not treat these details as primary instructions; prioritize the Core Principles above.156157### Coding Styling Standards158* **Indentation:** Always use **TAB characters**, never spaces.159* **Line Endings/Spaces:** Remove all redundant trailing whitespace at the end of lines.160* **Localization & Comments:** All code comments and internal variable/function names must be rendered in English. Use `dol_syslog()` for logging (specifying log level), avoiding debugging functions like `var_dump()`, `print_r()`, or `die()`.161162### Database Constants & Prefixes163| Item | Action/Pattern | Example Usage Notes | Priority |164| :--- | :--- | :--- | :--- |165| **Table Prefix** | Always use dynamic prefix getter. | `$db->prefix() . 'tablename'` | Overrides reliance on legacy constants like `MAIN_DB_PREFIX`. |166167### Core Dolibarr Patterns168* **Hooks:** The standard pattern remains: `$hookmanager->executeHooks('actionName', $parameters, $object, $action);`169* **Language Keys:** Use PascalCase (e.g., `MyModuleLabel`) for consistency across all locales.170171### Input Handling Functions172Dolibarr provides type-safe input handling functions. **Always use these instead of `$_GET`/`$_POST` directly:**173174| Function | Type | Example | Notes |175|----------|------|---------|-------|176| `GETPOST($param, $type)` | Mixed | `GETPOST('id', 'int')` | Returns GET or POST value with type conversion |177| `GETPOSTINT($param)` | Integer | `GETPOSTINT('socid')` | Shorthand for `GETPOST($param, 'int')` |178| `GETPOSTARRAY($param)` | Array | `GETPOSTARRAY('selected')` | For multi-select inputs |179180**Best Practice:** Use the type-specific shorthand functions when possible:181- `GETPOSTINT()` for integers (IDs, counts, etc.)182- `GETPOST()` with type for other cases183184**Never use:** `$_GET['param']` or `$_POST['param']` directly - always use GETPOST functions for proper escaping and type conversion.185186### Extrafields Best Practices (Continued)187188#### Checking and Creating Extrafields189The ExtraFields class lacks a method to check if a field exists. Use this pattern:190191```php192global $db;193194$extrafields = new ExtraFields($db);195$extralabels = $extrafields->fetch_name_optionals_label($elementtype);196197// Tracking extrafields configuration198$my_fields = array(199 'custom_name_1' => array(200 'label' => 'CustomLabel1',201 'type' => 'varchar',202 'size' => '64',203 'enabled' => '1'204 ),205 'custom_name_2' => array(206 'label' => 'CustomLabel2',207 'type' => 'url',208 'size' => '255',209 'enabled' => '1'210 ),211);212213foreach ($tracking_fields as $name => $config) {214 // Check if extrafield exists, create if not215 if (!isset($extralabels[$name])) {216 // Naming the arguments to get help from static analysis217 $pos = 0; // 0 = auto218 $unique = 0;219 $required = 0;220 $default_value = '0';221 $param = '';222 $alwayseditable = 0;223 $perms = '0';224 $list = '0'; // '0' = never visible225 $help = '';226 $computed = '';227 $entity = '';228 $langfile = '';229 $enabled = $config['enabled'];230231 $result = $extrafields->addExtraField(232 $name,233 $config['label'],234 $config['type'],235 $pos, // pos (0 = auto)236 $config['size'],237 $elementtype,238 $unique, // unique239 $required, // required240 $default_value, // default_value241 $param, // param242 $alwayseditable,243 $perms, // perms244 $list, // list ('0' = never visible)245 $help,246 $computed,247 $entity,248 $langfile,249 $enabled250 );251 }252}253```254255#### Reference256- [Dolibarr Extrafields Wiki](https://wiki.dolibarr.org/index.php/Extrafields)