---
source_hash: "8a596ece"
title: "PHP Development (backend)"
weight: 240
---

# 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
<?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:

```php
// 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
<?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`:

```php
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
<?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](/smartauth) for complete configuration.

## PATCH Method

The router supports the PATCH method in addition to GET, POST, PUT and DELETE:

```php
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)](/back) - Complete route documentation
- [Dolibarr Mapping](/back/mapping-dolibarr-react) - dm* classes in detail
- [SmartAuth](/smartauth) - JWT authentication
- [React Development](/howto/devfront) - Front-end
