Skip to main content

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:

  1. Slim (new): Routes defined in server/src/config/routes.php. Controllers live in server/src/App/Controllers/, services in server/src/App/Services/.
  2. Legacy (classic): PHP scripts under server/src/App/Legacy/. Routed by server/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() or include of 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_once in 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:

DatabaseNamingPurposeExample Tables
CentralunpispasIdentity, sessions, global dataUSER, STAFF, PLACE, ADDRESS, SESSION, CONNECTED
TenantP + 8-digit place IDPer-place operational dataPRODUCT, CATEGORY, TICKET, TICKETPRODUCT, QRCODE, PROTECTED

Connection Naming

VariableDatabase
$conx_globalCentral database
$conx_tenantCurrent place's tenant database
$conx_serverServer-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: 0 for success, non-zero for application errors.
  • payload: Only present on success.
  • HTTP status is always 200 for application-level errors — the errorcode in 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:

  1. The client sends local changes via POST to the sync endpoint.
  2. The server commits changes in a DB transaction.
  3. The server polls (up to ~20 seconds) for updates from other devices.
  4. 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:

  1. Create a Controller + Service in the Slim structure.
  2. Define the route in server/src/config/routes.php.
  3. Update the frontend to call the new route.
  4. 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

AreaSlimLegacy
Login / AuthLoginController / LoginServiceLegacy/login/, Legacy/common/authorize.php
DeviceDeviceControllerLegacy/device/
ConnectionConnectionController / ConnectionService
SyncSyncController / SyncServicemodel/synchronize.php, synchloader.php
PlacePlaceController / PlaceServiceLegacy/place/*
Catalogplace/productlst, place/productnfo
Ticketsplace/ticketnfo, place/ticketlst, device/getticket
Protected dataGET /protect/get, POST /protect/add (Slim)