PHP Development (backend)
SmartBoot adds to your Dolibarr module the entire technical stack to expose a REST API.
File Structure
After SmartBoot, your module contains:
monmodule/
├── smartmaker-api-prepend.php # Initialization file
├── pwa/
│ ├── api.php # API router
│ └── .htaccess # Apache redirection
└── smartmaker-api/
└── Controllers/ # Your controllers
smartmaker-api-prepend.php
This file initializes the SmartMaker environment:
- Required Dolibarr headers
- SmartAuth autoloader loading
- JWT layer initialization
- Your module's class autoloader
Important
Do not modify this file unless you know what you are doing.
pwa/api.php - The router
Route Syntax
Route::action(path, Controller::class, method, protected);
| Parameter | Description |
|---|---|
action |
get, post, put, delete |
path |
API path (ex: items, items/{id}) |
Controller::class |
PHP class to call |
method |
Class method |
protected |
true = authentication required |
Complete Example
<?php
require_once '../smartmaker-api-prepend.php';
use SmartAuth\Api\AuthController;
use SmartAuth\Api\RouteController as Route;
use MonModule\Api\ItemController;
use MonModule\Api\FileController;
// === Authentication Routes ===
Route::get('login', AuthController::class, 'index'); // Login info
Route::post('login', AuthController::class, 'login'); // Login
Route::get('refresh', AuthController::class, 'refresh'); // Refresh token
Route::post('logout', AuthController::class, 'logout', true);// Logout
Route::post('device', AuthController::class, 'device', true);// Register device
// === Business Routes ===
// CRUD Items
Route::get('items', ItemController::class, 'index', true);
Route::get('items/{id}', ItemController::class, 'show', true);
Route::post('items', ItemController::class, 'create', true);
Route::put('items/{id}', ItemController::class, 'update', true);
Route::delete('items/{id}', ItemController::class, 'delete', true);
// Search with filters
Route::post('items/search/{filter}', ItemController::class, 'search', true);
// Files
Route::get('file/{element}/{id}/{filename}', FileController::class, 'download', true);
// Fallback - Access denied
json_reply('Access denied', 403);
Dynamic Parameters
Segments in braces are passed in the payload:
// Route: GET /items/123
Route::get('items/{id}', ItemController::class, 'show', true);
// In the controller:
public function show($payload)
{
$id = $payload['id']; // 123
}
pwa/.htaccess
Redirects all requests to api.php (Apache >= 2.2.16):
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ api.php [QSA,L]
Note
For nginx, equivalent configuration is required in the server configuration file.
Create a Controller
Basic Structure
<?php
// smartmaker-api/Controllers/ItemController.php
namespace MonModule\Api;
class ItemController
{
/**
* List of items
* GET /items
*/
public function index($payload = null)
{
global $db, $user;
$items = [];
$sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "monobject";
$sql .= " WHERE entity = " . $user->entity;
$sql .= " ORDER BY date_creation DESC";
$resql = $db->query($sql);
while ($obj = $db->fetch_object($resql)) {
$item = new \MonObject($db);
$item->fetch($obj->rowid);
$mapping = new dmMonObject();
$items[] = $mapping->exportMappedData($item);
}
return [$items, 200];
}
/**
* Item detail
* GET /items/{id}
*/
public function show($payload = null)
{
global $db;
$id = $payload['id'] ?? null;
if (!$id) {
return ['ID required', 400];
}
$item = new \MonObject($db);
$res = $item->fetch($id);
if ($res <= 0) {
return ['Not found', 404];
}
$item->fetch_optionals();
$item->fetch_lines();
$mapping = new dmMonObject();
$data = $mapping->exportMappedData($item);
return [$data, 200];
}
/**
* Create an item
* POST /items
*/
public function create($payload = null)
{
global $db, $user;
$item = new \MonObject($db);
$item->label = $payload['label'] ?? '';
$item->description = $payload['description'] ?? '';
$item->fk_user = $user->id;
$res = $item->create($user);
if ($res < 0) {
return [$item->error ?: 'Creation failed', 500];
}
return [['id' => $res], 201];
}
/**
* Update an item
* PUT /items/{id}
*/
public function update($payload = null)
{
global $db, $user;
$id = $payload['id'] ?? null;
$item = new \MonObject($db);
$res = $item->fetch($id);
if ($res <= 0) {
return ['Not found', 404];
}
// Update provided fields
if (isset($payload['label'])) {
$item->label = $payload['label'];
}
if (isset($payload['description'])) {
$item->description = $payload['description'];
}
$res = $item->update($user);
if ($res < 0) {
return [$item->error ?: 'Update failed', 500];
}
return ['Updated', 200];
}
/**
* Delete an item
* DELETE /items/{id}
*/
public function delete($payload = null)
{
global $db, $user;
$id = $payload['id'] ?? null;
$item = new \MonObject($db);
$res = $item->fetch($id);
if ($res <= 0) {
return ['Not found', 404];
}
$res = $item->delete($user);
if ($res < 0) {
return [$item->error ?: 'Delete failed', 500];
}
return ['Deleted', 200];
}
/**
* Search with filters
* POST /items/search/{filter}
*/
public function search($payload = null)
{
global $db, $user;
$filter = $payload['filter'] ?? 'all';
$limit = $payload['limit'] ?? 20;
$offset = $payload['offset'] ?? 0;
$sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "monobject";
$sql .= " WHERE entity = " . $user->entity;
// Apply filters
switch ($filter) {
case 'active':
$sql .= " AND status = 1";
break;
case 'draft':
$sql .= " AND status = 0";
break;
case 'mine':
$sql .= " AND fk_user = " . $user->id;
break;
}
// Text search
if (!empty($payload['search'])) {
$search = $db->escape($payload['search']);
$sql .= " AND (label LIKE '%$search%' OR description LIKE '%$search%')";
}
$sql .= " ORDER BY date_creation DESC";
$sql .= " LIMIT " . (int)$limit . " OFFSET " . (int)$offset;
$resql = $db->query($sql);
$items = [];
while ($obj = $db->fetch_object($resql)) {
$item = new \MonObject($db);
$item->fetch($obj->rowid);
$mapping = new dmMonObject();
$items[] = $mapping->exportMappedData($item);
}
return [$items, 200];
}
}
Access the Connected User
The JWT user is available via $payload['user'] or the global variable $user:
public function create($payload = null)
{
global $user;
// $user is the complete Dolibarr User object
dol_syslog("Action by: " . $user->login);
// Get the ID
$userId = $user->id;
// Check permissions
if (!$user->rights->monmodule->write) {
return ['Permission denied', 403];
}
}
Create a Mapping Class (dm*)
dm* classes transform Dolibarr objects into JSON for React:
<?php
// smartmaker-api/dmMonObject.php
namespace MonModule\Api;
class dmMonObject extends \SmartAuth\DolibarrMapping\dmBase
{
use \SmartAuth\DolibarrMapping\dmTrait;
protected $type = "object";
protected $parentClassName = 'MonObject';
// For extrafields
protected $parentClassToUseForExtraFields = "MonObject";
protected $parentElementToUseForExtraFields = "monobject";
// Mapping: Dolibarr field => JSON field
protected $listOfPublishedFields = [
'rowid' => 'id',
'ref' => 'ref',
'label' => 'label',
'description' => 'description',
'fk_soc' => 'thirdparty', // Automatically resolved
'fk_statut' => 'status',
'date_creation' => 'created_at',
// Extrafields
'options_myfield' => 'my_field',
];
public function __construct()
{
global $langs;
$langs->load("monmodule@monmodule");
$this->boot();
}
/**
* Transform a value before sending
* Magic method: fieldFilterValue + FieldName
*/
public function fieldFilterValueCreatedAt($object)
{
return dol_print_date($object->date_creation, 'dayhour');
}
}
CORS Support
SmartAuth automatically manages CORS headers. For cross-origin requests in development, configure in Dolibarr administration:
| Constant | Dev Value |
|---|---|
SMARTAUTH_CORS_ORIGIN |
http://localhost:5173 |
See SmartAuth for complete configuration.
PATCH Method
The router supports the PATCH method in addition to GET, POST, PUT and DELETE:
Route::patch('items/{id}', ItemController::class, 'partialUpdate', true);
Route Cache
SmartAuth caches route resolution to improve performance. The cache is automatically invalidated when route files are modified.
See Also
- Back (PHP) - Complete route documentation
- Dolibarr Mapping - dm* classes in detail
- SmartAuth - JWT authentication
- React Development - Front-end