Adding a Modules
PHPTRAVELS v10 — Module & Supplier Backend Developer Guide
A complete backend guide to how modules work in PHPTRAVELS v10 and how to add a new module or supplier to any service (stays, flights, tours, cars, ferries, esim, bus…). This document is backend-focused: routing, database, credentials, search adapters, pagination, markup, currency, booking, payment, invoicing, and admin wiring.

1. Two Integration Models — Local Inventory vs Live API
Every service on the platform is powered by one of two model types. The mechanics are almost identical; the only real difference is where the inventory comes from.
Local Inventory
Live API Supplier
Source of data
Your own MySQL tables
A third-party API
How it's fetched
Medoo ($db->get / select)
cURL request to the supplier
Credentials
none
required — stored on the modules row (c1–c5) and sent with every cURL call
Example
bus, local stays/hotels, cars/cars
hotelbeds, duffel, cartrawler, airalo
modules row
one row per service (e.g. bus)
one row per supplier (e.g. hotelbeds)
Adapter folder
usually none (query tables inline)
modules/{type}/{supplier}/
Local example (query the DB directly):
Live-API example (read credentials from the module row, then cURL):
Both models ultimately return the same normalized result shape to the listing UI, so everything downstream (filters, booking, payment, invoice) is shared and identical.
2. Architecture Overview — Two PHP Apps
There are two separate front controllers, each with its own router:
Main app —
index.php(repo root). Handles all normal pages and APIs: home, listing, booking, invoice, admin,/api/.... Routes are registered fromapp/routes/_routes.php.Module sub-app —
modules/index.php(+modules/.htaccess). Handles everything under/modules/.... It has its ownRouterinstance andincludes each supplier'sindex.phpto register supplier endpoints (search, issue, cancel, etc.). It exposes$db(Medoo) and$pdo($db->pdo).
⚠️ Critical: a request to
modules/{type}/{supplier}/searchis served bymodules/index.php, not the main app. If you register a supplier endpoint on the main app router it will never fire for a/modules/...URL — it must be included frommodules/index.php.
Request lifecycle (booking): search → listing (aggregate suppliers / query DB) → cache draft (logs_bookings) → booking page → submit (create bookings row) → payment → callback (confirm + PNR) → invoice / PDF voucher.
3. Tech Stack, Conventions & Database Connection
PHP 8.2, Medoo ORM, Alpine.js + Tailwind on the front, MPDF for PDFs.
Medoo
$dbis created inconfig.phpfrom.env:
.env keys: DB_TYPE, DB_HOST, DB_DATABASE, DB_USERNAME, DB_PASSWORD. Inside the module sub-app, $pdo = $db->pdo; gives the raw PDO handle.
Translations: every visible string uses
T::key. TheTclass throws a fatal "Undefined constant" if the key does not exist — the?? 'fallback'only covers keys that exist but are null. Always add new keys toapp/lang/en.json(and keep the other language files in sync). See §20.Admin lists are built with the
crud()fluent builder (see §19).
4. The modules Table — Registering a Module
modules Table — Registering a ModuleEvery module/supplier is one row in modules. This row controls activation, credentials, markup, currency, and dev/live mode.
Full column reference (SHOW COLUMNS FROM modules):
id
int
PK
name
varchar
Unique identifier / adapter folder name (bus, hotelbeds, cartrawler)
active
enum('1','0')
Shown on the site
type
enum
Service group: flights, stays, tours, cars, bus, rail, cruises, visa, insurance, umrah, hajj, events, esim, ferries
status
enum('1','0')
API/module enabled
c1–c6
text
Generic credential slots (API key, secret, org id…)
dev_mode
enum('1','0')
Sandbox vs production
payment_mode
enum('1','0')
Supplier settle/payment mode flag
currency
varchar
Currency the supplier returns prices in
module_color
varchar
Tab/badge colour (hex)
order
int
Display order in tabs
prn_type
enum('1','0')
PNR handling flag
markup_type_b2c
enum('percentage','fixed')
Public markup type
markup_b2c
int
Public markup value
markup_type_b2b
enum('percentage','fixed')
Agent markup type
markup_b2b
varchar
Agent markup value
icon
varchar
Material Symbols icon name
check_balance
enum('1','0')
Wallet/balance check flag
content_import / import_database
enum / tinyint
Static-content import flags
logging_enabled
tinyint
Per-module request logging
host, database, username, password
varchar
For DB-import suppliers (direct DB feed)
tax, tax_type
int / enum('fixed','percentage')
Per-module tax
ancillaries_enabled, emd_enabled
tinyint
Flights extras (bags/seats, EMD)
Local module row (one per service):
Live supplier row (one per supplier; credentials filled later in the admin):
Active modules are cached: index.php loads $GLOBALS['modules'] via appCacheRemember('modules:active', 60, …) (60s APCu cache). New rows appear within a minute (or clear the cache).
4A. Feature Columns by Module Type — What to Enable for Which Module
The modules table is shared by every service, so it carries the union of all features. Any single module only uses a subset. The admin edit screen (app/views/admin/settings/modules/manage.php) shows every toggle, so when you add a module you must enable only the columns that make sense for its type (and for whether it's local or live). Enabling an irrelevant feature is usually harmless (the code that reads it never runs for that type) but confusing — enabling the wrong one (e.g. a supplier DB import for a live-only API) can cause errors.
Columns every module uses (all types, local + live)
name, type, icon, module_color, order
Identity + how it appears in tabs
active
Show/hide on the public site
status
Module/API enabled
currency
Currency the module's prices are in
markup_b2c / markup_type_b2c, markup_b2b / markup_type_b2b
Selling markup (public + agent)
tax / tax_type
Optional per-module tax
payment_mode
Enable/disable taking payment for this module
Columns only LIVE-API suppliers use (never for local inventory)
c1–c6
API credentials (key, secret, org id…)
Labelled per supplier in modules-settings.php (§18). Empty for local.
dev_mode
Sandbox ↔ production
Meaningless for local.
logging_enabled
Log API request/response
Only meaningful when there is an API.
check_balance
Verify supplier wallet/deposit before booking
For GDS/wallet suppliers (e.g. flight consolidators).
Type-specific feature columns
ancillaries_enabled
flights
Enable ancillary services (extra bags, seat selection). Read by isAncillariesEnabled() and the flights booking page.
emd_enabled
flights
Enable EMD (Electronic Miscellaneous Document) issuance. isEmdEnabled().
prn_type
flights (and ferries)
PNR handling mode for the supplier.
content_import
stays
Enable importing static hotel content (names, photos, geo, facilities) from the supplier — separate from live availability/pricing.
import_database
stays
Store that imported content in a separate database (recommended for large hotel catalogues so it doesn't bloat the main DB).
host, database, username, password
stays (only when import_database = 1)
Connection for that separate content database.
Airalo Packages tab / rules
esim
Per-country package availability + commission rules (esim-specific admin tab).
The admin edit tabs (which appear when)
The module edit screen is split into tabs, each mapping to files under app/views/admin/settings/modules/:
General (
manage.php) — identity,active/status/dev_mode/payment_mode, plus the feature toggles above. All types.Credentials (
credentials.php) — thec1–c5inputs (labelled viamodules-settings.php) and the Test button. Live suppliers only.Database (
database.php) —host/database/username/passwordfor the separate content DB. Stays suppliers withcontent_import+import_database.Markup (
markup.php) — B2C/B2B markup + tax. All types.Airalo Packages (
airalo-packages.php) — country/package/commission rules. eSIM (Airalo) only.
Quick "what to enable" by type
flights (live GDS/NDC): credentials
c1…,dev_mode,logging_enabled, and as the supplier supports —ancillaries_enabled,emd_enabled,prn_type,check_balance. No content/database import.stays (live hotel API): credentials,
dev_mode, and if you import static content,content_import(+import_databaseand the DB connection). No ancillaries/EMD/PRN.stays / cars / bus (local inventory): no credentials,
dev_mode, content/database, ancillaries, EMD, PRN, or check-balance — onlycurrency,markup_*,status,active,payment_mode.cars / tours / ferries (live): credentials,
dev_mode,logging_enabled. Ancillaries/ EMD/PRN/content-import do not apply.esim (Airalo): credentials,
dev_mode, plus the Packages tab for country/commission rules. No content-import/ancillaries/PRN.
Rule of thumb: start from the type.
content_import/import_database/DB fields are a stays concept;ancillaries_enabled/emd_enabled/prn_typeare flights; the Packages tab is esim.c1–c6,dev_mode,logging_enabled,check_balanceare live-supplier concepts (leave them off for local inventory).
5. Folder & File Placement (Where Everything Lives)
Three locations matter:
Local modules typically skip modules/{type}/{supplier}/ entirely (they query tables in app/routes/api/{type}/listingRoutes.php). A local module still creates a modules/{type}/{name}/index.php if it needs admin booking actions (see §17).
6. URL Structure & Routing
Main app uses vendor/qaxim/php-router. Routes are registered in app/routes/_routes.php (order matters — the router returns on the first match).
Typical registration order for a module (note catch-all placement):
.htaccess rewrites any non-file/non-dir request to index.php?url=$1; the router derives the path from REQUEST_URI minus the base dir.
Listing SEO URL examples:
Module sub-app (/modules/...) has its own router and includes each supplier index.php. Supplier endpoints are registered like:
Then add one line to modules/index.php:
7. Search / Listing Backend & Supplier Aggregation
The listing results are produced by aggregating all suppliers of a type (live) and/or querying local tables (local), then merging into one normalized array.
Supplier aggregation pattern (from app/routes/cars/listingRoutes.php):
The frontend then POSTs to each supplier's search endpoint in parallel: POST /modules/{type}/{supplier}/search.
Supplier search.php adapter contract:
Reads
$_POSTsearch params (origin, destination, dates, pax,page,per_page).Reads credentials from its
modulesrow ($db->get('modules', '*', ['name' => '…'])).Calls the supplier API via cURL.
Normalizes each result to the shared shape and applies markup (§9).
Returns JSON (array of results, or
{results, total, page, total_pages}).
Local listing (from app/routes/api/bus/listing) queries tables directly and returns the same shape — no supplier loop:
Normalized result shape (what every adapter must return per item): service_name / operator, origin, destination, times, duration, price (marked-up), currency, image, amenities, rating, source, and an id to book.
8. Pagination (Progressive Loading)
Suppliers are paginated per supplier, loaded progressively ("load more").
Backend contract (supplier search.php):
Accepts
$_POST['page'](default 1) and$_POST['per_page'](default 20–25).Returns that page of results, plus either a JSON envelope
{results, total, page, total_pages}or a plain array, and may set response headers such asX-Grand-Total,X-Current-Page,X-Per-Page.
Frontend loop (stays reference, app/views/modules/stays/listing/stays.php):
Keeps
currentPage[supplier]andhasMorePages[supplier].fetchSupplier(supplier, page)→POST modules/stays/{supplier}/searchwithpage+per_page.loadMoreHotels()increments the page for every supplier that stillhasMorePagesand fetches the next page in parallel, appending to the list.hasMoreis derived frompage < total_pages(envelope) or "got a full page of results" (array) — an empty array means no more pages.
For a local module, pagination is simple SQL LIMIT/OFFSET inside the listing route, or you can return everything at once if the volume is small.
9. Markup & Pricing — the MARKUP() Function
MARKUP() FunctionSelling price = supplier/base price + module markup. Use the shared helper:
Behaviour:
Reads
markup_b2c/markup_type_b2c(public) ormarkup_b2b/markup_type_b2b(logged-in agents) from themodulesrow.Supports percentage and fixed markup.
Honours agent custom markup (
users.apply_markup = 'custom'→markup_value/markup_type) which overrides the module markup.Optionally converts currency in the same call (
$fromCurrency→$toCurrency).
$module may be the module name (it will look up the row) or the already-fetched module array. Related helpers: calculateCompletePrice() (base + tax + markup) and CURRENCY_CONVERT() (see §10).
Local modules apply the same markup. Example (bus listing):
10. Currency & Conversion
Currencies live in the currencies table:
name
3-letter code (USD)
rate
Exchange rate relative to the default currency
default
1 on exactly one row — the base/settlement currency
status
1/0 active
Three currencies to keep straight:
Module/base currency — what the supplier returns (
modules.currency, e.g. supplier returns EUR). Payments are settled in the site default currency.Default currency — the
currenciesrow withdefault = 1.Display currency — the visitor's choice, in
$_SESSION['app_currency'].
Conversion helper:
Formula: converted = ($price / fromRate) * toRate (both rates from currencies, status = 1). If from/to are omitted it converts base → session display currency. The invoice summary and booking summary convert display amounts on the fly while storing the base amount in bookings.price_markup / currency_markup.
11. Cache Booking (Draft) — logs_bookings
logs_bookingsBefore a real booking is created, the selection is cached so the booking page has a stable, shareable URL and the checkout survives refreshes.
Flow (all modules):
POST /api/{type}/booking/save-draft→ insert intologs_bookings:Redirect to
/{type}/booking/{hash}— the booking page loads the draft fromlogs_bookingsby hash.On confirm, the submit endpoint reads the draft, creates the real
bookingsrow, and deletes the draft.
logs_bookings: hash, data (JSON draft), user_id, created_at.
12. Booking / Checkout Page & Shared Components
The checkout page (app/views/modules/{type}/booking/index.php) reuses shared includes — do not rewrite these:
views/includes/booking/booking-auth.php— Guest Details card (login / guest). Requires a$countriesarray defined by the page before the include, or it fatals and silently truncates the rest of the page:views/includes/booking/payment-methods.php— payment gateway selector (bindsformData.selected_payment).views/includes/booking/loading.php— full-page loader (showBookingLoader).Standard layout: left form column + sticky Booking Summary sidebar, a Booking Options card (special requests, terms), and the submit button at the page bottom.
Submit → POST /api/{type}/booking/submit.
13. Creating the Real Booking — the bookings Table
bookings TableThe submit endpoint validates the guest, computes pricing, and inserts one row into bookings. Key columns to set:
invoice_id
8-char public id (strtoupper(substr(bin2hex(random_bytes(4)),0,8)))
module / module_type
The service (bus / bus, or hotelbeds / stays)
booking_status
pending → confirmed (on payment)
payment_status
unpaid → paid / refunded
price_original
base (supplier) total — before markup
price_markup
customer total — after markup (settlement currency)
commission
price_markup − price_original → this is the admin Earning
currency_markup
Currency of the stored totals
first_name/last_name/email/phone/…
Lead guest
adults/childs/infants
Pax
booking_data
JSON snapshot of the draft (trip, dates, etc.)
travellers
JSON passenger list
pnr
Generated on confirmation (local) or returned by supplier
payment_gateway
Selected gateway id
Earnings depend on this: the admin bookings list shows
commission. If you store the customer total in bothprice_originalandprice_markup, earning shows0. Always setprice_original = base,price_markup = customer total,commission = difference.
14. Payment Flow
Invoice page Make Payment →
processPayment()posts to/payment/process(invoice_id,gateway_id)./payment/processcreates a payment token + transaction log and redirects to the secure/payment/{hash}page, which renders the gateway.The gateway redirects back to the module-aware callback URL. The prefix map lives in
app/routes/globalRoutes.php— add your module:The module invoice route handles
?payment_status=success|cancel|failure&token=…by callinghandle_payment_callback($token, $action, $extra)(withmodule_type/module), then sets a$_SESSION['payment_notice'].On success: mark
paid+confirmed, recordtransaction_id, and generate the PNR (local inventory has no supplier PNR, so generate one). Decrement inventory here.
15. Invoice Page & Shared Summary
app/routes/{type}/invoiceRoutes.php → GET /invoice/{type}/{id} loads the booking (scoped by module_type) and renders app/views/modules/{type}/invoice/index.php.
The right column is the shared component — include it, don't rebuild it:
It renders status chips, totals (with currency conversion), the gateway selector, and the Make Payment / Download Invoice / Resend / Request Cancellation buttons. The view must also define the standard JS the summary calls: processPayment(), downloadInvoice(), requestCancellation() (copy from any module, swap the module name in the endpoints).
16. PDF Voucher — GENERATE_BOOKING_PDF()
GENERATE_BOOKING_PDF()GENERATE_BOOKING_PDF($invoiceId) (in app/lib/functions.php) selects a per-module HTML template and renders it with MPDF to uploads/invoices/invoice_{id}.pdf. Add your module to the template map:
Create app/views/notifications/emails/{type}/booking_pdf.php (copy the cars template; it's the simplest). The template receives $booking, $bookingData, $travellersData, $settings, $invoiceId.
Download route: GET /api/{type}/booking/download-invoice/{id} — clear the output buffer before sending the PDF (while (ob_get_level()) ob_end_clean();) or a stray UTF-8 BOM corrupts the %PDF- header.
17. Module Actions — Issue / Cancel / Void / Refund
The shared admin booking editor (app/views/admin/bookings/edit.php) posts action buttons to /modules/{module_type}/{module}/{action} — handled by the module sub-app.
Live suppliers implement these by calling the supplier API (issue ticket, cancel PNR, request refund) — see
modules/cars/cartrawler/cancel.php,modules/esim/airalo/{issue,cancel}.php.Local modules implement them against the DB. Register the routes on the module sub-app router and add the include to
modules/index.php:
Each handler: admin-guard → load booking (scoped by module_type) → mutate status → return JSON {status:true, success:true, message:…}. On cancel/void, restore inventory (seats, availability) if it was decremented. Mark Paid / Mark Confirmed are handled generically by POST /admin/bookings/action/{invoice_id} (module-agnostic, already works).
18. Admin — Settings → Modules (Status, Credentials, Currency, Markup)
Settings → Modules (/admin/settings/modules, app/routes/admin/modulesRoutes.php) is where an admin manages every module row without SQL:
Toggle active / status (enable the module / its API).
Toggle dev_mode (sandbox ↔ production).
Set currency, markup_b2c/b2b and their types.
Enter credentials (
c1–c5), test them, and save.
Naming the credential fields (c1–c5 rename) — for LIVE suppliers
The generic c1–c5 columns are re-labelled per supplier in app/views/admin/settings/modules-settings.php via the $moduleCredentials map. This is where you tell the admin UI what each credential means for your API:
Each mapped
cNrenders a labelled input on the module edit screen; unmapped slots are hidden.api_testing/envtoggle the "Test credentials" button and sandbox/live switch.The Test button posts to your
modules/{type}/{supplier}/credsendpoint (creds.php), which validates the keys against the supplier (e.g. requests an OAuth token) and returns success/failure JSON.Saved credentials land in
modules.c1…c5; yoursearch.php/booking.phpread them via$db->get('modules', '*', ['name' => '{supplier}']).
Local modules simply set the credential map to false (no fields shown) and are configured only for markup/currency/status.
19. Admin — CRUD Lists, Sidebar, Bookings & Cancellations
Admin lists use the
crud()builder:->table(),->col(),->label(),->row()(string templates orfunction($row){…}formatters),->actions(),->custom_button(),->col_width(), and->extra_fetch('col1,col2')to make extra columns available to row formatters. Row-format expressions like{{myHelper(id)}}are eval'd — the parser skips identifiers that are real PHP functions.Sidebar: add the module menu, shown when the module is active.
Bookings list (
app/views/admin/bookings/bookings.php) is module-agnostic. It shows status badges and a "Cancellation Requested" badge whencancellation_request = 1andcancellation_status = 0.Dashboard counts pending cancellations with
cancellation_request = 1 AND cancellation_status = 0 AND booking_status != 'cancelled'.
20. Translations (i18n)
Use
T::keyeverywhere.T::key ?? 'Fallback'only guards null values — an undefined key throws a fatal, blanking the page.Add every new key to
app/lang/en.jsonand mirror it into the other language files (ar, tr, ru, fr, ch, de, es, it, pl, bg, vn) so counts stay in sync.Audit before shipping: grep each
T::in your files againsten.json.
21. Notifications, Emails & Webhooks
NOTIFY::booking($module, $booking, $data)andNOTIFY::payment(...)send the confirmation/payment emails (templates underapp/views/notifications/emails/{type}/).triggerWebhook('{type}/{event}', '{type}.{event}.name', $payload)fires outbound webhooks (e.g.booking.draft_created,payment.completed).
22. Step-by-Step: Add a LIVE API Supplier
Example: add supplier acme to stays.
Register the module row (or add it via Settings → Modules → Add):
Create the adapter folder
modules/stays/acme/with:index.php—requirethe files below.creds.php— registerstays/acme/creds: validatec1/c2against the API, return JSON.search.php— registerstays/acme/search: read$_POSTparams +page/per_page, readc1…c5from the module row, cURL the supplier, normalize +MARKUP(), return paginated JSON.booking.php/issue.php/cancel.php/refund.phpas needed.
Wire it into the sub-app: add
include __DIR__ . '/stays/acme/index.php';tomodules/index.php.Label the credentials: add an
acmeentry understaysinapp/views/admin/settings/modules-settings.php(c1 => ['label'=>'API Key', …]).Confirm the listing aggregates it: stays listing globs
modules/stays/*/search.php, so the new supplier is picked up automatically; the frontend fetchesmodules/stays/acme/searchper page.Actions: implement
issue/cancel/refundso the admin editor buttons work.Test: enter credentials in Settings → Modules → Test, run a search, book, pay in sandbox, check the invoice/PDF, and try Cancel/Refund.
The rest is free — booking cache, checkout page, payment, invoice, PDF, currency, and markup are shared and already work once search.php returns the normalized shape.
23. Step-by-Step: Add a LOCAL Inventory Module
Example: a new local module {type} (the bus/local-cars pattern).
Inventory tables — create your
{type}tables (+ append toinstall/db.sql), e.g. a listing table, a variants/routes table, a per-date calendar, and a settings table.Module row — insert a single local row (
name = {type},type = {type}, currency, markup). No credentials.Routes (
app/routes/{type}/…):homeRoutes,locationSuggestionRoutes(optional),listingRoutes(catch-all last),bookingRoutes(save-draft +/{type}/booking/{hash}submit),
invoiceRoutes; register them in_routes.phpbefore the catch-all.
Listing API (
app/routes/api/{type}/listing): query your tables with Medoo, apply the module markup, return the normalized shape. Guard past/unscheduled dates.Views (
app/views/modules/{type}/…): search widget, listing, booking (reuse shared includes +$countries), invoice (reuseincludes/invoice/summary.php).Admin —
crud()lists for your inventory, sidebar entry, and manage the module's markup/currency via Settings → Modules.Actions — create
modules/{type}/{type}/index.php+actions.phpregisteringissue/cancel/void/refundon the sub-app router; add the include tomodules/index.php.Payment/PNR/PDF — add your module to the callback prefix map (§14), generate a PNR on payment success, decrement inventory, and add the PDF template (§16).
24. Testing & Go-Live Checklist
php -levery touched file; validateapp/lang/en.json.All routes registered in
_routes.php; catch-all is last.Every
T::key exists in all language files.Edge cases: past dates, unscheduled dates, sold-out, empty results, expired draft hash, missing guest fields.
Payment sandbox round-trip →
paid+confirmed+transaction_id+ PNR + inventory decrement.Invoice loads; PDF downloads (valid
%PDF-header); Make Payment / Resend / Cancel work.Admin actions (Issue/Cancel/Void/Refund) return JSON and mutate correctly; cancellation request surfaces on list + dashboard.
Earnings (
commission) correct on the bookings list.install/db.sqlcontains the new tables + module row for a fresh install.
25. Appendix — Quick Reference
Key tables: modules (registration + c1–c5 + markup), bookings (final booking), logs_bookings (draft cache), currencies (rate/default), payment_gateways, booking_logs, plus your module's inventory tables.
Key files/paths:
config.php— Medoo$db.app/routes/_routes.php— main-app route registration order.app/routes/{type}/— module routes.app/views/modules/{type}/— module views.modules/index.php— module sub-app (add supplierincludes here).modules/{type}/{supplier}/— live adapter (index/creds/search/booking/issue/cancel).app/views/admin/settings/modules-settings.php— c1–c5 credential labels per supplier.app/views/includes/booking/*andapp/views/includes/invoice/summary.php— shared UI.app/lib/functions.php— helpers below.
Helper signatures:
MARKUP($price, $module, $db, $fromCurrency = null, $toCurrency = null)— apply markup (B2C/B2B/agent) + optional conversion.calculateCompletePrice($basePrice, $moduleName, $db, $from = null, $to = null)— base + tax + markup.CURRENCY_CONVERT($price, $db, $fromCurrency = null, $toCurrency = null)→['price','converted','conversion_rate'].GENERATE_BOOKING_PDF($invoiceId)— render the per-module PDF voucher.handle_payment_callback($token, $action, $extra)— process a gateway callback.
Normalized listing result (per item): source, route_id/id, service_name/ operator, origin, destination, departure_time, arrival_time, duration, price (marked-up), currency, img, amenities[], rating, plus any id needed to build the booking draft.
26. Technical Reference (Conventions, Constants, Boilerplate)
26.1 Naming conventions
Route files:
{action}Routes.phpinapp/routes/{type}/—homeRoutes.php,listingRoutes.php,detailRoutes.php,bookingRoutes.php,invoiceRoutes.php,locationSuggestionRoutes.php. API routes live inapp/routes/api/{type}/.View folders:
app/views/modules/{type}/with{type}-search.php,index.php,listing/,booking/,invoice/,details/.Admin views:
app/views/admin/{type}/({plural}.phpfor lists,manage.php/{singular}.phpfor forms). PDF templates:app/views/notifications/emails/{type}/booking_pdf.php.Supplier adapters:
modules/{type}/{supplier}/withindex.php,creds.php,search.php,booking.php,issue.php,cancel.php,refund.php,void.php.Sub-app routes are named without a leading slash and without
modules/:$router->post('{type}/{supplier}/search', …). The public URL is/modules/{type}/{supplier}/search.Helper functions: shared cross-cutting helpers are
UPPER_SNAKE_CASE(MARKUP,CURRENCY_CONVERT,GENERATE_BOOKING_PDF,ADMIN_AUTH,NOTIFY), module-local helpers arecamelCase(busRoutesForBus,busModuleAction). Always guard definitions withif (!function_exists('name'))in files that may be included twice.DB tables: module inventory is prefixed by type —
bus,bus_routes,bus_routes_calendar,bus_settings;stays,stays_rooms,stays_rooms_calendar.Translation keys:
snake_caseinapp/lang/*.json, referenced asT::snake_case.
26.2 Global constants & route-file boilerplate
Defined in config.php:
root
site base URL (e.g. http://localhost/v10/)
build URLs / redirects
admin
'admin'
admin URL segment → root . admin . '/bookings'
views
__DIR__ . '/app/views/'
require_once views . 'includes/header.php'
uploads
__DIR__ . '/uploads/'
file paths / uploads in templates
Every route file follows this shape. $router, $db (Medoo) and $SECURE are already in scope (the file is require_once'd from _routes.php):
Admin routes must call ADMIN_AUTH() first (see 26.5).
26.3 Router inclusion order (app/routes/_routes.php)
app/routes/_routes.php)Files are require_once'd top-to-bottom; the router returns on the first matching route, so specific paths must be included before catch-alls, and the generic module gateway is last:
Sub-app: register the supplier include in modules/index.php alongside the others:
26.4 Router error / JSON response contract
The main router has a global error handler; the module sub-app returns a structured JSON 404: {"error":true,"code":404,"message":"Endpoint not found","path":"…","method":"…"}. Adapter/action endpoints should always answer with:
edit.php's handleAction() treats status === true || success === true as success.
26.5 Security — CSRF & admin auth
ADMIN_AUTH()(infunctions.php) redirects tologinunless$_SESSION['user_id']is set and$_SESSION['user_role'] === 'admin'. Call it at the top of every admin route.For JSON admin endpoints in the sub-app (no redirect), guard inline:
CSRF (
app/lib/Csrf.php): put<?= CSRF::tokenField() ?>in admin forms and verify withCSRF::validateToken($_POST['csrf_token'])(alsoCSRF::getToken(),CSRF::generateToken(),CSRF::verifyRequest()). Public booking/listing AJAX endpoints typically use their own hash/session validation rather than CSRF tokens.
26.6 Session keys & caching
$_SESSION['user_id'],$_SESSION['user_role'](admin/agent/ user),$_SESSION['admin_logged_in'](true),$_SESSION['app_currency'](display currency),$_SESSION['app_language'](i18n),$_SESSION['payment_notice'](invoice flash),$_SESSION['booking_data']/$_SESSION['booking_hash'](checkout).Cache helper:
appCacheRemember($key, $ttl, callable $resolver)(APCu, seconds). Active modules use keymodules:active(60s). Clear/expire it after changing module rows.
26.7 crud() admin builder — method reference
crud() admin builder — method referenceecho crud()->table('…') … ->render(); Chainable methods:
Notes: displayed columns are validated against real table columns (invalid → exception); the status/featured toggles POST to crud's own toggle_status handler; row-format expressions are eval'd after substituting {{col}} values, and identifiers that are real PHP functions are skipped (so {{myHelper(id)}} calls your function).
26.8 Full table schemas (for fresh installs / reference)
bookings (the final booking record):
logs_bookings (draft cache): id, hash, data longtext, created_at, updated_at, user_id.
currencies: id, name varchar(3), country, default enum('1','0'), status enum('1','0'), rate.
payment_gateways: id, status, name, c1–c5 text, dev_mode, currency, order, active, note, type enum(credit_card,…,module_gateway), module, default enum('1','0'). (Gateways use the same c1–c5 credential pattern as modules; the module_gateway type is for supplier-hosted payment.)
booking_logs (supplier action/error trail): id, booking_id, action varchar(100), details longtext, created_at. Referenced by the admin editor's "Booking Issue Logs" panel.
26.9 Code skeletons
Supplier search.php (modules/{type}/{supplier}/search.php):
Admin action handler (modules/{type}/{module}/actions.php, local module):
Admin editor action map (app/views/admin/bookings/edit.php, for reference): buttons post Mark Paid/Mark Confirmed → /admin/bookings/action/{invoice_id} (generic), and Issue/Void/Cancel/Refund → /modules/{module_type}/{module}/{action} (sub-app).
Last updated