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.phpDatabase Access Pattern
ADOdb - Non Query Builder
Questo progetto usa ADOdb con query SQL raw, NON il query builder di CodeIgniter.
Connessioni Database
| Connessione | Uso | Metodi |
|---|---|---|
$this->db_slave | Lettura (SELECT) | GetArray(), GetRow(), GetOne() |
$this->db | Scrittura (INSERT/UPDATE/DELETE) | Execute(), Insert_ID() |
Metodi ADOdb
// 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(); // integerEscaping Parametri
Usa sempre la funzione escape() per prevenire SQL injection:
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).
Step 1: Model (Symlink)
Per le anagrafiche condivise con elerama, si usa un symlink al model esistente:
# Crea symlink al model esistente
cd src/models/admin/
ln -s ../../../../elerama/src/modules/admin/models/categories_model.php Categories_model.phpIl model esistente in elerama fornisce i metodi necessari:
// 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.
// 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.
// 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
// 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:
// src/response/ErpApiResponseFactory.php
public static function categories()
{
require_once 'contexts/erp/ErpCategoriesResponses.php';
return new ErpCategoriesResponses();
}Pattern Comuni
Validazione Input
$schema = QueryStringValidator::merge(
QueryStringValidator::requiredString('code'),
QueryStringValidator::requiredString('description'),
QueryStringValidator::requiredInt('parentId')
);
QueryStringValidator::validate($params, $schema, ErpApiResponseFactory::categories());Check Dipendenze prima di Delete
// 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
// 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
// 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
// Success
return ServiceResponseFactory::ok($data, 'context', 'action');
// Error
return ServiceResponseFactory::fail('context', 'errorCode', $data, ServiceErrorType::NotFound);4. Cast esplicito degli ID
(int) $id
(int) $row['ID_CATEGORY']5. Usare db_slave per le letture
// 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
- Ogni libreria anagrafica DEVE esporre un metodo
getForFilters() - Le altre librerie caricano la libreria e usano il metodo
Metodo Standard getForFilters()
Ogni libreria anagrafica deve implementare:
// 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
// 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
| Aspetto | Pattern Corretto | Pattern Errato |
|---|---|---|
| Query | Centralizzata in un punto | Duplicata in ogni libreria |
| Manutenzione | Modifica in un solo file | Modifica in N file |
| Performance | Query 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.