Core concepts
Six ideas carry the whole platform. Learn them once and the rest of Tiger reads as the obvious consequence — which is the point: there's a right way to do each thing, and it's the boring way.
Each section is the short version. The link at the end of each goes deep.
1. Four layers, one direction
Zend_* → the ZF1 engine (TigerZF) vendor/…/tigerzf Tiger-owned
Tiger_* → the platform (kernel + substrate) vendor/…/tiger-core Tiger-owned
App_* → your shared, route-less code the app library yours
Modules → your features (routes + UI) application/modules/* yours
Tiger_* is the platform layer — Tiger_Application, Tiger_Acl_Acl, Tiger_Service_Service,
Tiger_Form, Tiger_Model_Table — Composer-autoloaded from vendor/ exactly like Zend_*. App_*
is your shared plumbing: base classes, helpers, integrations, anything with no route of its own.
The killer use is subclassing Tiger's bases once so every module extends yours:
class App_Service_Base extends Tiger_Service_Service { /* your house rules, everywhere */ }
Dependencies point one way — modules → core, never the reverse. Core needs no app; every app needs core.
Rule of thumb: has a route/UI → a module. No route → the app library.
2. Ownership: vendor/ is Tiger's, everything else is yours
There is exactly one prime directive:
Every file is owned by exactly one party, and the boundary is enforced by tooling.
Composer physically cannot write outside vendor/, so your code is safe from composer update by
construction. From that falls the golden rule: extend, don't edit. You customize by adding —
a module, a config override, a subclass, a skin — never by patching a framework file. Nothing stops
you editing vendor/; it just vanishes on the next update. We don't forbid, we make the consequence
predictable.
The platform marks its surface so you know what you can lean on: @api is stable and
semver-guaranteed; @internal may change in any release. Build on @api.
The question to ask whenever you're unsure where something goes: "Who owns this, and what happens
to it on composer update?" The answer is usually the design.
→ The drop-in architecture · Modules
3. Tenancy is a membership row
Three tables, one big idea:
| Entity | What | Note |
|---|---|---|
org |
the tenant | self-referential parent_org_id → hierarchies |
user |
a person | deliberately thin — identity only |
org_user |
the membership | the tenancy boundary and the role carrier |
A user isn't in an org because of an org_id column — they're in it because an org_user row
exists. So cross-tenant denial is structural: no membership row, no access, everywhere, without
a check you could forget to write.
And because the role lives on the membership, the same person is admin in one org and viewer
in another with no gymnastics. The role is resolved fresh from org_user on every request, so a
revoked membership takes effect on the next one — no stale sessions.
The corollary you must not break: never add columns to user or org. They're Tiger-owned.
Extend identity from your own module with an FK-linked table.
→ Orgs, users & membership · Authorization
4. The config cascade — and the live-override pattern
Four tiers, each owned by the right party, merged later-wins:
core.ini (vendor) Tiger framework plumbing — you never touch it
← application.ini (your app) you app settings + overrides
← local.ini (your app) you secrets / per-deploy (gitignored)
← the `config` DB table runtime global or per-org, live, no deploy
Overriding a Tiger default means declaring it later in the cascade, not forking core. And the bottom tier is a table, so config changes at runtime:
$cfg = new Tiger_Model_Config();
$cfg->set(Tiger_Model_Config::SCOPE_ORG, $orgId, 'tiger.skin', 'jaguar'); // live next request
That shape — files are the base, a DB table is the runtime override tier, last wins — is the
Tiger pattern, and it repeats deliberately: translations (Tiger_Model_Translation), CMS content, a
theme's menus.ini vs. authored menus, and every settings screen you'll ever build. When you add
your own settings surface, mirror it instead of inventing a table.
Its sibling is the option tier (Tiger_Model_Option) — same (scope, scope_id, key, value)
shape, but lazy: read only when its owner asks. Settings that shape the request go in config;
per-user or per-entity state (a saved layout, a dismissed notice) goes in option. And prefix
every key with your module slug — that's what lets Tiger sweep your rows when the module is removed.
5. A theme is just a path
Two axes, deliberately different weights:
| Theme | Skin | |
|---|---|---|
| Is | a whole view layer (layouts, view scripts) | a CSS-only override |
| Weight | heavy, changes rarely | light, structurally inert |
| Per-tenant | rare (white-label) | yes — the branding axis |
The active theme resolves from the config cascade at bootstrap and is then just a path woven into the layout path, the view-script paths, and the asset base URL. No inheritance, no routing — the only fallback is theme → core default views, so a theme provides only what it wants to override. That one cheap trick is what makes a whole rendering approach swappable: core emits data plus semantic default views, and the theme decides how it looks.
A skin is a :root { --bs-* } overlay, so it swaps at runtime with no rebuild — and per-org theming
is nothing more than an org-scoped config row (see #4: it's the same mechanism).
6. /api — the message says where it's going
There is one endpoint, POST /api, and the routing metadata travels inside the message
alongside the payload. You send module + service + method; Tiger_Ajax_ServiceFactory builds
the class name from those segments, the ACL authorizes the call (resource = the service class,
privilege = the method name, deny-by-default), and the named method receives the whole payload
as $params. It writes its answer with _success() / _error() / _formErrors() into the one
standard envelope every response uses: {result, data, redirect, form, messages}. Adding a method to
a service ships an endpoint — no route to register, no path to design, no version to juggle. The
inverse of that convenience is the discipline: thin controllers, fat services, and every mutation
is validate a form → wrap the writes in _transaction().
→ Webservices (the /api pattern)
The vocabulary
| Term | Means |
|---|---|
| Module | a self-contained feature folder in application/modules/<name>/ — controllers, services, models, views, ACL, migrations, i18n. Purely additive |
| App library | your shared, route-less App_* code — base classes, helpers, integrations |
| Service | a Tiger_Service_Service subclass; the /api-reachable, ACL-gated unit where logic lives |
| Form | a Tiger_Form subclass declaring an elements() schema; validates at submit and on blur |
| Model | a Tiger_Model_Table subclass — one class per table, with UUID keys, actor/timestamp stamps, and soft-delete built in |
| Org / membership | the tenant, and the org_user row that grants access to it and carries the role |
| Config tier | the eager config table — the runtime bottom of the .ini cascade, global or per-org |
| Option tier | the lazy option table — per-user/per-entity state, read on demand |
| Theme / skin | the view layer / the CSS-variable overlay on top of it |
@api / @internal |
the stable surface you build on / the part that may change |
Next
Ready to build? Building on Tiger is the hub, and Your first module is the five-minute proof.