---
source_hash: "7e9b1249"
title: "Chapter 2: Controllers"
weight: 640
---

# Chapter 2: Controllers

Controllers contain the business logic of the API.

## Controller Structure

```php
<?php
// smartmaker-api/Controllers/ItemController.php

namespace MonModule\Api;

class ItemController
{
    public function __construct() {}

    /**
     * Item list
     * @param array|null $payload
     * @return array [data, httpCode]
     */
    public function index($payload = null)
    {
        global $db, $user;

        // Logic here

        return [['items' => $items], 200];
    }
}
```

## Available Global Variables

In protected routes:

- `$db`: Dolibarr database connection
- `$user`: logged Dolibarr user (via JWT)
- `$conf`: Dolibarr configuration
- `$langs`: translations

## Complete CRUD

```php
<?php
namespace MonModule\Api;

use MonModule\Api\Mappers\dmProduct;

class ProductController
{
    /**
     * Product list
     */
    public function index($payload = null)
    {
        global $db, $user;

        require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';

        // Pagination parameters
        $limit = $payload['limit'] ?? 50;
        $offset = $payload['offset'] ?? 0;

        // SQL query
        $sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "product";
        $sql .= " WHERE entity IN (" . getEntity('product') . ")";
        $sql .= " ORDER BY label ASC";
        $sql .= " LIMIT " . (int) $limit;
        $sql .= " OFFSET " . (int) $offset;

        $resql = $db->query($sql);
        $products = [];

        if ($resql) {
            $mapper = new dmProduct();

            while ($obj = $db->fetch_object($resql)) {
                $product = new \Product($db);
                $product->fetch($obj->rowid);

                $products[] = $mapper->exportMappedData($product);
            }
        }

        return [['products' => $products], 200];
    }

    /**
     * Product detail
     */
    public function show($payload = null)
    {
        global $db;

        $id = $payload['id'] ?? null;

        if (!$id) {
            return ['ID required', 400];
        }

        require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';

        $product = new \Product($db);
        $res = $product->fetch($id);

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

        // Load extrafields
        $product->fetch_optionals();

        $mapper = new dmProduct();
        $data = $mapper->exportMappedData($product);

        return [$data, 200];
    }

    /**
     * Create a product
     */
    public function create($payload = null)
    {
        global $db, $user;

        // Validation
        if (empty($payload['label'])) {
            return ['Label required', 400];
        }

        require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';

        $product = new \Product($db);
        $product->ref = $payload['ref'] ?? '';
        $product->label = $payload['label'];
        $product->description = $payload['description'] ?? '';
        $product->price = $payload['price'] ?? 0;
        $product->status = $payload['status'] ?? 1;

        // Create in database
        $res = $product->create($user);

        if ($res < 0) {
            return ['Error creating product: ' . $product->error, 500];
        }

        // Save extrafields
        if (!empty($payload['extrafields'])) {
            foreach ($payload['extrafields'] as $key => $value) {
                $product->array_options['options_' . $key] = $value;
            }
            $product->insertExtraFields();
        }

        return [['id' => $res], 201];
    }

    /**
     * Update a product
     */
    public function update($payload = null)
    {
        global $db, $user;

        $id = $payload['id'] ?? null;

        if (!$id) {
            return ['ID required', 400];
        }

        require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';

        $product = new \Product($db);
        $res = $product->fetch($id);

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

        // Update provided fields
        if (isset($payload['label'])) {
            $product->label = $payload['label'];
        }
        if (isset($payload['description'])) {
            $product->description = $payload['description'];
        }
        if (isset($payload['price'])) {
            $product->price = $payload['price'];
        }
        if (isset($payload['status'])) {
            $product->status = $payload['status'];
        }

        $res = $product->update($product->id, $user);

        if ($res < 0) {
            return ['Error updating product: ' . $product->error, 500];
        }

        // Update extrafields
        if (!empty($payload['extrafields'])) {
            $product->fetch_optionals();
            foreach ($payload['extrafields'] as $key => $value) {
                $product->array_options['options_' . $key] = $value;
            }
            $product->updateExtraFields();
        }

        return ['Updated', 200];
    }

    /**
     * Delete a product
     */
    public function delete($payload = null)
    {
        global $db, $user;

        $id = $payload['id'] ?? null;

        if (!$id) {
            return ['ID required', 400];
        }

        require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';

        $product = new \Product($db);
        $res = $product->fetch($id);

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

        // Check permissions
        if (!$user->hasRight('produit', 'supprimer')) {
            return ['Permission denied', 403];
        }

        $res = $product->delete($user);

        if ($res < 0) {
            return ['Error deleting product: ' . $product->error, 500];
        }

        return ['Deleted', 200];
    }
}
```

## Search with Filters

```php
/**
 * Advanced search
 */
public function search($payload = null)
{
    global $db;

    require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';

    // Filters
    $search = $payload['search'] ?? '';
    $category = $payload['category'] ?? null;
    $status = $payload['status'] ?? null;
    $minPrice = $payload['minPrice'] ?? null;
    $maxPrice = $payload['maxPrice'] ?? null;

    // Pagination
    $limit = min($payload['limit'] ?? 50, 100);  // Max 100
    $offset = $payload['offset'] ?? 0;

    // Build query
    $sql = "SELECT p.rowid FROM " . MAIN_DB_PREFIX . "product as p";

    // Join category if needed
    if ($category) {
        $sql .= " INNER JOIN " . MAIN_DB_PREFIX . "categorie_product as cp";
        $sql .= " ON p.rowid = cp.fk_product";
    }

    $sql .= " WHERE p.entity IN (" . getEntity('product') . ")";

    // Filters
    if ($search) {
        $sql .= " AND (p.label LIKE '%" . $db->escape($search) . "%' ";
        $sql .= " OR p.ref LIKE '%" . $db->escape($search) . "%')";
    }

    if ($category) {
        $sql .= " AND cp.fk_categorie = " . (int) $category;
    }

    if ($status !== null) {
        $sql .= " AND p.tosell = " . (int) $status;
    }

    if ($minPrice !== null) {
        $sql .= " AND p.price >= " . (float) $minPrice;
    }

    if ($maxPrice !== null) {
        $sql .= " AND p.price <= " . (float) $maxPrice;
    }

    $sql .= " ORDER BY p.label ASC";
    $sql .= " LIMIT " . (int) $limit;
    $sql .= " OFFSET " . (int) $offset;

    $resql = $db->query($sql);
    $products = [];
    $mapper = new dmProduct();

    while ($obj = $db->fetch_object($resql)) {
        $product = new \Product($db);
        $product->fetch($obj->rowid);
        $products[] = $mapper->exportMappedData($product);
    }

    return [['products' => $products], 200];
}
```

## File Management

```php
/**
 * Download a file
 */
public function download($payload = null)
{
    global $conf, $db;

    $element = $payload['element'];     // e.g., 'product'
    $parentId = $payload['parentId'];
    $ref = $payload['ref'];             // file name

    // Build path
    $dir = $conf->$element->dir_output;
    $filepath = $dir . '/' . $parentId . '/' . $ref;

    if (!file_exists($filepath)) {
        return ['File not found', 404];
    }

    // Return file as base64
    $content = file_get_contents($filepath);
    $mime = mime_content_type($filepath);
    $base64 = base64_encode($content);

    return [[
        'filename' => $ref,
        'mime' => $mime,
        'content' => 'data:' . $mime . ';base64,' . $base64
    ], 200];
}

/**
 * Upload a file
 */
public function upload($payload = null)
{
    global $conf, $user;

    $element = $payload['element'];
    $parentId = $payload['parentId'];
    $filename = $payload['filename'];
    $content = $payload['content'];  // base64

    // Decode content
    $data = base64_decode(preg_replace('#^data:.+;base64,#', '', $content));

    // Destination path
    $dir = $conf->$element->dir_output . '/' . $parentId;

    // Create directory if needed
    if (!is_dir($dir)) {
        dol_mkdir($dir);
    }

    $filepath = $dir . '/' . $filename;

    // Save
    if (file_put_contents($filepath, $data) === false) {
        return ['Error saving file', 500];
    }

    return [['path' => $filepath], 201];
}
```

## Permission Checking

```php
public function delete($payload = null)
{
    global $db, $user;

    // Check specific permission
    if (!$user->hasRight('monmodule', 'delete')) {
        return ['Permission denied', 403];
    }

    // Check if admin
    if (!$user->admin) {
        return ['Admin required', 403];
    }

    // Rest of code...
}
```

## Logging

```php
public function create($payload = null)
{
    global $db, $user;

    // Debug log
    dol_syslog("ProductController::create by " . $user->login, LOG_DEBUG);

    // On error
    dol_syslog("ProductController::create error: " . $product->error, LOG_ERR);

    // ...
}
```

## Key Points to Remember

1. **Return [data, code]**: always an array with data and HTTP code
2. **$payload** contains URL parameters and JSON body
3. **Use mappers** to convert Dolibarr objects
4. **Validate inputs** before processing
5. **Check permissions** for sensitive actions

[Previous Chapter](/training/module8-backend-api/routage) | [Back to Module](/training/module8-backend-api) | [Next Chapter: Mappers ->](/training/module8-backend-api/mappers)
