Backend Integration
This guide describes the PHP backend architecture, how the frontend communicates with it, and the key patterns a developer needs to understand when working across the stack.
Architecture Overview
The backend lives in server/ and serves as a REST API for the unpispas multi-tenant system. It is organised in three layers:
Two Stacks
The backend runs two routing stacks simultaneously:
- Slim (new): Routes defined in
server/src/config/routes.php. Controllers live inserver/src/App/Controllers/, services inserver/src/App/Services/. - Legacy (classic): PHP scripts under
server/src/App/Legacy/. Routed byserver/public/index.php— if the URL path matches a Legacy script, that script is included and executed directly.
New features are implemented in Slim. Legacy scripts remain operational until their functionality is migrated.
Controllers
Located in server/src/App/Controllers/.
- Handle HTTP only: parse request body/query, call a Service, return a
Response. - Inject services via constructor (e.g.
LoginService,LoggerFactory). - Never access the database directly — no
new ddbb()orincludeof legacy scripts.
class LoginController
{
public function __construct(
private LoginService $loginService,
private LoggerFactory $loggerFactory
) {}
public function login(Request $request, Response $response): Response
{
$body = json_decode($request->getBody()->getContents(), true);
$result = $this->loginService->login($body);
$response->getBody()->write(json_encode($result));
return $response->withHeader('Content-Type', 'application/json');
}
}
Services
Located in server/src/App/Services/.
- Hold business logic and orchestrate work.
- Inject
DdbbService,LoggerFactory, and other dependencies in the constructor. - Return data (arrays/objects) — the controller turns it into HTTP.
- May use action files in service-specific subfolders (e.g.
Services/login/) for gradual migration from legacy. - Load all legacy dependencies via
require_oncein the constructor — never use lazy loading.
Service Subfolders
Small action files under Services/<domain>/ receive $ddbb and $logger from the parent service. They wrap legacy calls during migration and are eventually replaced with direct implementations.
Legacy
Located in server/src/App/Services/legacy/ and server/src/App/Legacy/.
- Reusable library code kept during migration.
- Used only via Services or action files — never called from Controllers.
- Not exposed as direct HTTP endpoints in Slim.
Database Architecture
The backend uses a multi-database model:
| Database | Naming | Purpose | Example Tables |
|---|---|---|---|
| Central | unpispas | Identity, sessions, global data | USER, STAFF, PLACE, ADDRESS, SESSION, CONNECTED |
| Tenant | P + 8-digit place ID | Per-place operational data | PRODUCT, CATEGORY, TICKET, TICKETPRODUCT, QRCODE, PROTECTED |
Connection Naming
| Variable | Database |
|---|---|
$conx_global | Central database |
$conx_tenant | Current place's tenant database |
$conx_server | Server-level connection |
Connections are managed through ConxHelper and must be explicitly released in try/finally blocks.
REST API Response Contract
All API responses follow a consistent JSON structure:
{
"errorcode": 0,
"payload": { }
}
errorcode:0for success, non-zero for application errors.payload: Only present on success.- HTTP status is always 200 for application-level errors — the
errorcodein the body distinguishes success from failure. Do not change HTTP status codes for business logic errors.
Frontend ↔ Backend Communication
The frontend uses adhocService.DoRequest() (from upp-data) to make REST calls to the backend:
const result = await this.adhoc.DoRequest('place/productlst', {
place: placeId,
session: sessionId,
});
if (result.errorcode === 0) {
// use result.payload
}
Sync Endpoint
The primary data synchronisation mechanism uses a long-poll pattern:
- The client sends local changes via POST to the sync endpoint.
- The server commits changes in a DB transaction.
- The server polls (up to ~20 seconds) for updates from other devices.
- Returns any new/modified data to the client.
Connection State
- Stored in CONNECTED table (central DB) or APCu cache.
- Clients create a connection (
connection_create), then use it for sync polling. - Connections track which tables/objids the client cares about (triggers).
- When changes are committed, affected connections are marked
REFRESH.
Key Concepts
Place
A "locale" or business site. Metadata lives in the central DB (PLACE); all operational data lives in the tenant DB (P + 8-digit objid).
Session
Identifies the user and optionally the device/QR code. Stored in the central DB (SESSION). Validated before any place or tenant operation.
Ticket Numbering
- POS (LOGIN): The client assigns ticket numbers. On startup with an empty cache, it fetches the last numbers from
device/getticket.php. - Guest (QR): The server assigns ticket numbers.
- Provisional (X) → Definitive (S/T): Unpaid tickets use provisional numbering (series
X). On payment, the client replaces with definitive numbering and creates the fiscal registration.
Routing: Migration Strategy
When migrating a legacy endpoint to Slim:
- Create a Controller + Service in the Slim structure.
- Define the route in
server/src/config/routes.php. - Update the frontend to call the new route.
- Remove or stub the legacy script.
Never add path-specific wrappers in index.php to redirect legacy URLs to Slim. Keep the routing contract explicit in config/routes.php and the frontend code.
Main Functional Areas
| Area | Slim | Legacy |
|---|---|---|
| Login / Auth | LoginController / LoginService | Legacy/login/, Legacy/common/authorize.php |
| Device | DeviceController | Legacy/device/ |
| Connection | ConnectionController / ConnectionService | — |
| Sync | SyncController / SyncService | model/synchronize.php, synchloader.php |
| Place | PlaceController / PlaceService | Legacy/place/* |
| Catalog | — | place/productlst, place/productnfo |
| Tickets | — | place/ticketnfo, place/ticketlst, device/getticket |
| Protected data | — | GET /protect/get, POST /protect/add (Slim) |