Skip to content

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.

The drop-in architecture


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.

Config: .ini + DB overrides


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).

Theming & skins


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.