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