Skip to content

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 moduleapplication/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 libraryApp_* 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 & schemaTiger_Model_Table, UUID v7 vs v4 keys, the standard columns, soft-delete, the query-builder rule, and migrations.
  • Webservices (the /api pattern) — 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 tokensBearer auth for a stateless, non-browser client.

Platform services you shouldn't rebuild

  • Localization (i18n) — semantic, owner-prefixed keys; never a hardcoded string.
  • MailTiger_Mail, config-driven transport.
  • Location & countriesTiger_Location (geocode / reverse / IP) and Tiger_I18n_Country, behind one provider-agnostic facade.
  • LoggingTiger_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 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 by composer 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 / org from your own module with an FK-linked table.
  • Zero-infrastructure activation. A module never touches Apache/nginx, DNS, or anything outside its own directory.