Building on Tiger
This is the hub for actually building something. It answers three questions — where does my code go?, what's the loop?, and which guide covers this step? — and then hands you off. Nothing here is repeated from the deep pages; every line is a signpost.
New to the platform? Read Core concepts first — it's the vocabulary the rest of these pages assume.
First decision: a module, or the app library?
| You're adding… | It goes in… | Because |
|---|---|---|
| a feature with a URL, an API, views, or ACL | a module — application/modules/<name>/ |
it's a routed, self-contained unit that plugs in by convention |
| shared, route-less code (base classes, helpers, an integration wrapper) | the app library — App_* |
it has no surface of its own; modules consume it |
| a base class every module should extend | the app library (App_Service_Base extends Tiger_Service_Service) |
customize Tiger's extension points once, without touching core |
| a whole rendering approach (layouts, view scripts) | a theme | presentation is a path, not a feature |
| a helper function you just want available app-wide | the Code Area (/code) |
superadmin-gated snippets, no module ceremony |
Rule of thumb, every time: does it have a route? Route → module. No route → library.
→ Modules for the folder reference and lifecycle · The drop-in architecture for why the split exists.
The canonical loop
Building a feature in Tiger is the same seven steps, in the same order, every time. Do them in order and the framework does the rest.
| # | Step | The move | Deep dive |
|---|---|---|---|
| 1 | Scaffold | vendor/bin/tiger make:module billing — never hand-build the folder |
Modules |
| 2 | Schema | an additive-only NNNN_name.php migration in migrations/, with a real down |
Models & schema |
| 3 | Model | Billing_Model_Invoice extends Tiger_Model_Table — finders built on activeSelect(), never a raw SQL string |
Models & schema |
| 4 | Service | Billing_Service_Invoice extends Tiger_Service_Service — the /api-reachable unit; validate → _transaction() → _success() |
Webservices |
| 5 | Form | Billing_Form_Invoice extends Tiger_Form — declare elements() once; it validates at submit and on blur |
Forms & validation |
| 6 | ACL | a rule in configs/acl.ini — /api is deny-by-default, so a service with no rule is simply refused |
Authorization |
| 7 | Surface | a thin controller + .phtml views (SSR shell only), or an admin screen; the browser then talks to /api |
Admin screens · Routing |
Then vendor/bin/tiger module:activate billing and it's live at its canonical path with zero route
registration and zero infrastructure changes.
The full worked example — a table, a service method, the ACL rule, settings storage, and a clean uninstall — is How to write your first module. For the five-minute version of the same idea, start at Your first module.
The map — which guide covers what
The build loop
- Modules — the folder, auto-discovery, activation, dependency alerts. The unit of everything.
- Models & schema —
Tiger_Model_Table, UUID v7 vs v4 keys, the standard columns, soft-delete, the query-builder rule, and migrations. - Webservices (the
/apipattern) — one endpoint, the message pattern, the response envelope, and why it isn't REST. - Forms & validation — declarative
elements(), convenience validation on blur, and the batteries (DB-uniqueness, password policy, reCAPTCHA, country picker). - Authorization — deny-by-default ACL: resource = the class, privilege = the method or action.
- Routing & pretty URLs — the canonical path is free; a vanity URL is declared, never hand-registered, and never a web-server rewrite.
Surfaces
- Building an admin screen — the five-piece back-office template every module follows, so third-party screens look like core's.
- Theming & skins — theme vs. skin, theme-as-a-path, cache-busted assets.
- Custom menus — admin-authored navigation, rendered auth-filtered and localized.
- Custom fields — declarative field groups on CMS content, with no schema change.
- CMS authoring — pages, layouts, partials, shortcodes, and the visual builder your content will live in.
Reaching your API from outside
- API discovery (OpenAPI) — a spec generated from your services and forms, filtered to the caller's role.
- API tokens —
Bearerauth for a stateless, non-browser client.
Platform services you shouldn't rebuild
- Localization (i18n) — semantic, owner-prefixed keys; never a hardcoded string.
- Mail —
Tiger_Mail, config-driven transport. - Location & countries —
Tiger_Location(geocode / reverse / IP) andTiger_I18n_Country, behind one provider-agnostic facade. - Logging —
Tiger_Log, structured JSON, auto-enriched, never throws. - Crypto & secrets — the encryption key and the pepper, and how they rotate with no downtime.
- Third-party libraries — vendoring an SDK for hosts with no Composer.
Before you build anything that feels like plumbing, check whether the platform already ships it — that list is longer than it looks.
Ship it
- The
bin/tigerconsole — migrations, secrets, module management, scaffolding. - Deployment — getting it onto a box (including shared hosting).
- Marketplaces & sources — distributing your module to other installs.
The house rules
Short version of what the deep pages enforce. Break these and you're fighting the framework:
- Extend, don't edit. Never change a file under
vendor/to change app behavior — it's replaced bycomposer update. Add a module, a config override, a subclass, or a skin. - Thin controllers, fat services. A controller renders the initial shell; logic lives in the
service. Never page-POST a form to a controller — the UI is a client that calls
/api. - Validate, then transact. Every mutation is
isValid()→_transaction()→_success(), and errors come back as_error()/_formErrors()with a translation key, never a bare string. - Declare the ACL when you write the call. Deny-by-default means an undeclared service is dead on
arrival; an in-method
_isAdmin()check is defense-in-depth, not the gate. - Never a raw SQL string. Query builder with bound parameters, always.
- Never a hardcoded string, role, or setting. Translation keys, ACL data, and config — owner-prefixed with your module slug so your rows are attributable (and sweepable).
- Never widen a core table. Extend
user/orgfrom your own module with an FK-linked table. - Zero-infrastructure activation. A module never touches Apache/nginx, DNS, or anything outside its own directory.