Skip to content

Guida: Creare una Nuova API ​

Questa guida spiega come creare una nuova API REST per ERP seguendo i pattern stabiliti.

Checklist Rapida ​

[ ] 1. Model      → src/models/{module}/{Resource}_model.php (symlink a elerama)
[ ] 2. Library    → src/libraries/erp/{module}/{Resource}_api_lib.php
[ ] 3. Controller → src/controllers/erp/{module}/{Resource}.php
[ ] 4. Response   → src/response/contexts/erp/Erp{Resource}Responses.php
[ ] 5. Factory    → Aggiungere metodo in ErpApiResponseFactory.php

Database Access Pattern ​

ADOdb - Non Query Builder

Questo progetto usa ADOdb con query SQL raw, NON il query builder di CodeIgniter.

Connessioni Database ​

ConnessioneUsoMetodi
$this->db_slaveLettura (SELECT)GetArray(), GetRow(), GetOne()
$this->dbScrittura (INSERT/UPDATE/DELETE)Execute(), Insert_ID()

Metodi ADOdb ​

php
// Lista di record
$rows = $this->db_slave->GetArray($query);  // array di array

// Singolo record
$row = $this->db_slave->GetRow($query);     // array o false

// Singolo valore
$value = $this->db_slave->GetOne($query);   // scalar o false

// Esecuzione query (INSERT/UPDATE/DELETE)
$result = $this->db->Execute($query);       // true/false

// ID dell'ultimo insert
$id = $this->db->Insert_ID();               // integer

Escaping Parametri ​

Usa sempre la funzione escape() per prevenire SQL injection:

php
escape($value, 'integer')  // Per numeri interi
escape($value, 'string')   // Per stringhe (aggiunge apici)

Esempio: API Categories ​

Creiamo l'API per gestire le categorie (/erp/admin/categories).


Per le anagrafiche condivise con elerama, si usa un symlink al model esistente:

bash
# Crea symlink al model esistente
cd src/models/admin/
ln -s ../../../../elerama/src/modules/admin/models/categories_model.php Categories_model.php

Il model esistente in elerama fornisce i metodi necessari:

php
// Metodi tipici di un model esistente
$this->CI->categories_model->read_categories($companyId);
$this->CI->categories_model->read_category($companyId, $id);
$this->CI->categories_model->save_category($companyId, $code, $description);
$this->CI->categories_model->update_category($id, $code, $description);
$this->CI->categories_model->delete_category($id);

Step 2: Library ​

La library contiene la business logic.

php
// src/libraries/erp/admin/Categories_api_lib.php
<?php

defined('BASEPATH') or exit('No direct script access allowed');

class Categories_api_lib
{
    private $CI;
    private const CONTEXT = 'categories';

    public function __construct()
    {
        $this->CI = &get_instance();
        $this->CI->load->model('admin/Categories_model');
    }

    public function list(int $companyId): ServiceResponse
    {
        $result = $this->CI->categories_model->read_categories($companyId);

        $categories = [];
        while (!$result->EOF) {
            $categories[] = [
                'id' => $result->fields['ID_CATEGORY'],
                'code' => $result->fields['C_CATEGORY'],
                'description' => $result->fields['D_CATEGORY'],
            ];
            $result->MoveNext();
        }

        return ServiceResponseFactory::ok($categories, self::CONTEXT, 'list');
    }

    public function create(int $companyId, string $code, string $description): ServiceResponse
    {
        // Validate uniqueness
        if ($this->CI->categories_model->list_category_code_in_use($companyId, $code)) {
            return ServiceResponseFactory::fail(
                self::CONTEXT,
                'duplicateCode',
                ['field' => 'code', 'value' => $code],
                ServiceErrorType::Validation,
                'Codice categoria gia esistente'
            );
        }

        $categoryId = $this->CI->categories_model->save_category($companyId, $code, $description);

        if (!$categoryId) {
            return ServiceResponseFactory::failDbInsert(self::CONTEXT);
        }

        return ServiceResponseFactory::ok($categoryId, self::CONTEXT, 'create');
    }

    public function update(int $companyId, int $id, string $code, string $description): ServiceResponse
    {
        $category = $this->CI->categories_model->read_category($companyId, $id);

        if (!$category) {
            return ServiceResponseFactory::fail(
                self::CONTEXT,
                'notFound',
                ['id' => $id],
                ServiceErrorType::NotFound,
                'Categoria non trovata'
            );
        }

        // Validate uniqueness (excluding current)
        if ($this->CI->categories_model->list_category_code_in_use($companyId, $code, $id)) {
            return ServiceResponseFactory::fail(
                self::CONTEXT,
                'duplicateCode',
                ['field' => 'code', 'value' => $code],
                ServiceErrorType::Validation,
                'Codice categoria gia esistente'
            );
        }

        $this->CI->categories_model->update_category($id, $code, $description);

        return ServiceResponseFactory::ok([
            'id' => $id,
            'code' => $code,
            'description' => $description
        ], self::CONTEXT, 'update');
    }

    public function delete(int $companyId, int $id): ServiceResponse
    {
        $category = $this->CI->categories_model->read_category($companyId, $id);

        if (!$category) {
            return ServiceResponseFactory::fail(
                self::CONTEXT,
                'notFound',
                ['id' => $id],
                ServiceErrorType::NotFound,
                'Categoria non trovata'
            );
        }

        // Check if in use
        if ($this->CI->categories_model->is_category_in_use($id)) {
            return ServiceResponseFactory::fail(
                self::CONTEXT,
                'inUse',
                ['id' => $id],
                ServiceErrorType::Conflict,
                'Impossibile eliminare: categoria in uso'
            );
        }

        $this->CI->categories_model->delete_category($id);

        return ServiceResponseFactory::ok($id, self::CONTEXT, 'delete');
    }
}

Step 3: Controller ​

Il controller gestisce HTTP request/response.

php
// src/controllers/erp/admin/Categories.php
<?php

defined('BASEPATH') or exit('No direct script access allowed');

require APPPATH . 'libraries/erp/Erp_controller.php';

class Categories extends Erp_controller
{
    private $lib;

    public function __construct()
    {
        parent::__construct();

        $this->CI->load->library('erp/admin/Categories_api_lib');
        $this->lib = &$this->CI->categories_api_lib;
    }

    /**
     * GET /erp/admin/categories
     */
    public function index_get()
    {
        $response = $this->lib->list(
            $this->auth->get_active_company_id()
        );

        ErpApiResponseFactory::categories()->auto($response)->toJson();
    }

    /**
     * POST /erp/admin/categories
     */
    public function index_post()
    {
        $params = $this->post();

        $schema = QueryStringValidator::merge(
            QueryStringValidator::requiredString('code'),
            QueryStringValidator::requiredString('description')
        );

        QueryStringValidator::validate($params, $schema, ErpApiResponseFactory::categories());

        $response = $this->lib->create(
            $this->auth->get_active_company_id(),
            $params['code'],
            $params['description']
        );

        ErpApiResponseFactory::categories()->auto($response)->toJson();
    }

    /**
     * PUT /erp/admin/categories/{id}
     */
    public function id_put($id)
    {
        $params = $this->put();

        $schema = QueryStringValidator::merge(
            QueryStringValidator::requiredString('code'),
            QueryStringValidator::requiredString('description')
        );

        QueryStringValidator::validate($params, $schema, ErpApiResponseFactory::categories());

        $response = $this->lib->update(
            $this->auth->get_active_company_id(),
            (int) $id,
            $params['code'],
            $params['description']
        );

        ErpApiResponseFactory::categories()->auto($response)->toJson();
    }

    /**
     * DELETE /erp/admin/categories/{id}
     */
    public function id_delete($id)
    {
        $response = $this->lib->delete(
            $this->auth->get_active_company_id(),
            (int) $id
        );

        ErpApiResponseFactory::categories()->auto($response)->toJson();
    }
}

Step 4: Response Context ​

php
// src/response/contexts/erp/ErpCategoriesResponses.php
<?php

class ErpCategoriesResponses extends ErpCommonResponses
{
    protected string $responseContext = 'categories';
}

Step 5: Response Factory ​

Aggiungere il metodo in ErpApiResponseFactory.php:

php
// src/response/ErpApiResponseFactory.php

public static function categories()
{
    require_once 'contexts/erp/ErpCategoriesResponses.php';
    return new ErpCategoriesResponses();
}

Pattern Comuni ​

Validazione Input ​

php
$schema = QueryStringValidator::merge(
    QueryStringValidator::requiredString('code'),
    QueryStringValidator::requiredString('description'),
    QueryStringValidator::requiredInt('parentId')
);

QueryStringValidator::validate($params, $schema, ErpApiResponseFactory::categories());

Check Dipendenze prima di Delete ​

php
// Nella library
if ($this->CI->categories_model->is_category_in_use($id)) {
    return ServiceResponseFactory::fail(
        self::CONTEXT,
        'inUse',
        ['id' => $id],
        ServiceErrorType::Conflict,
        'Impossibile eliminare: categoria in uso'
    );
}

Best Practices ​

1. Sempre filtrare per Company ​

php
// Nel controller - ottieni company dalla sessione
$companyId = $this->auth->get_active_company_id();

// Passa sempre il companyId alla library
$response = $this->lib->list($companyId);

2. Validare unicita escludendo il record corrente ​

php
// Nella library - per update
if ($this->CI->categories_model->list_category_code_in_use($companyId, $code, $excludeId)) {
    return ServiceResponseFactory::fail('categories', 'duplicateCode', ...);
}

3. Usare ServiceResponseFactory per le risposte ​

php
// Success
return ServiceResponseFactory::ok($data, 'context', 'action');

// Error
return ServiceResponseFactory::fail('context', 'errorCode', $data, ServiceErrorType::NotFound);

4. Cast esplicito degli ID ​

php
(int) $id
(int) $row['ID_CATEGORY']

5. Usare db_slave per le letture ​

php
// Lettura - usa sempre db_slave
$data = $this->db_slave->GetArray($query);

// Scrittura - usa db
$result = $this->db->Execute($query);

Accesso Cross-Library (Pattern Filtri) ​

Quando una libreria necessita di dati da un'altra anagrafica (es. brands per filtri articoli), utilizzare il pattern centralizzato invece di duplicare le query SQL.

Pattern Corretto ​

  1. Ogni libreria anagrafica DEVE esporre un metodo getForFilters()
  2. Le altre librerie caricano la libreria e usano il metodo

Metodo Standard getForFilters() ​

Ogni libreria anagrafica deve implementare:

php
// Nella library (es. Brands_api_lib.php)
public function getForFilters(int $companyId): array
{
    $result = $this->CI->brands_model->read_brands($companyId);

    $brands = [];
    while (!$result->EOF) {
        $brands[] = [
            'id' => $result->fields['ID_BRAND'],
            'code' => $result->fields['C_BRAND'],
            'description' => $result->fields['D_BRAND'],
        ];
        $result->MoveNext();
    }

    return $brands;
}

Utilizzo da Altra Libreria ​

php
// In un'altra libreria (es. Articles_api_lib.php)
class Articles_api_lib
{
    public function __construct()
    {
        $this->CI = &get_instance();

        // Carica librerie per accesso centralizzato ai filtri
        $this->CI->load->library('erp/admin/Brands_api_lib', [], 'brands_lib');
        // $this->CI->load->library('erp/admin/Categories_api_lib', [], 'categories_lib');
    }

    public function getFilters(int $companyId): ServiceResponse
    {
        $filters = [
            'brands' => $this->CI->brands_lib->getForFilters($companyId),
            // 'categories' => $this->CI->categories_lib->getForFilters($companyId),
        ];

        return ServiceResponseFactory::ok($filters, 'articles', 'filters');
    }
}

Vantaggi ​

AspettoPattern CorrettoPattern Errato
QueryCentralizzata in un puntoDuplicata in ogni libreria
ManutenzioneModifica in un solo fileModifica in N file
PerformanceQuery ottimizzata (solo campi necessari)Query pesante con tutti i campi

File Esempio

Vedi src/libraries/erp/examples/Filters_example_lib.php per un esempio completo commentato.

Documentazione interna Elerama