Modules

A module is how you add a feature to Tiger. If it has controllers, routes, an API, views, or ACL β€” it's a module. If it's shared, route-less plumbing β€” it's the app library instead. This page is the module reference; for a hands-on first run see Your first module.

A module is a folder

Modules live in application/modules/<name>/ and are auto-discovered β€” Tiger scans the modules dir, includes each Bootstrap.php, and runs it. There's no central registry to edit, no "register your module here" step.

application/modules/billing/
β”œβ”€β”€ Bootstrap.php            # runs on boot; hook route overrides, nav, settings here
β”œβ”€β”€ controllers/             # Billing_*Controller β€” the thin MVC surface (SSR shell only)
β”œβ”€β”€ services/                # Billing_Service_* β€” the /api surface (fat: logic lives here)
β”œβ”€β”€ models/                  # Billing_Model_* β€” extend Tiger_Model_Table
β”œβ”€β”€ forms/                   # Billing_Form_* β€” extend Tiger_Form
β”œβ”€β”€ views/scripts/           # .phtml view scripts
β”œβ”€β”€ configs/
β”‚   β”œβ”€β”€ module.ini           # module settings
β”‚   β”œβ”€β”€ acl.ini              # permissions (deny-by-default)
β”‚   └── navigation.ini       # admin sidebar entries (ACL-filtered)
β”œβ”€β”€ migrations/              # NNNN_name.php, additive-only
β”œβ”€β”€ languages/en/billing.php # ['billing.key' => 'text']
└── assets/                  # symlinked to public/_modules/billing on activate

Purely additive β€” it touches no core file

This is the contract that makes modules safe:

  • Config β€” the module's configs/*.ini merge into the global config. It doesn't edit anyone's config; it adds to the cascade.
  • Behavior β€” it exposes Module_Service_* classes; the core /api gateway resolves them by convention. Thin controllers, fat services.
  • Permissions β€” resources are class names, gated by the ACL; the module ships its own acl.ini.
  • Views / i18n / routes β€” same additive pattern, all namespaced to the module.

Because nothing in a module reaches into Tiger, and nothing Tiger ships reaches into a module, composer update can replace the framework under you without disturbing your feature. Arrows point one way: modules β†’ core, never the reverse.

Activation is zero-infrastructure

vendor/bin/tiger module:activate billing      # on
vendor/bin/tiger module:deactivate billing    # off
vendor/bin/tiger module:list                  # what's installed / active

On activate, a module's assets/ (if present) are symlinked to public/_modules/<slug> β€” reference them at /_modules/billing/… (and route them through $this->asset() for cache-busting). A module never touches Apache/nginx, DNS, or anything outside its own dir. That's a hard rule: infra-touching activation would break 1-click install. A pretty public URL is a PHP-layer route override, not a rewrite rule.

Modules can also declare lightweight dependency alerts in configs/dependency.ini β€” activate warns if a required module is off; deactivate warns what depends on this one. They're convenience alerts, never hard blocks.

Scaffold, don't hand-write

Don't build the folder by hand β€” generate it:

vendor/bin/tiger make:module billing

You get a live controller + /api service + ACL + views + config to build from. Then it's the loop from Your first module: add a method to the service, and it's callable.

Where things go

You're adding… It belongs in…
a feature with a URL / API / views a module (application/modules/*)
shared, route-less code (base classes, helpers, integrations) the app library (library/App/*)
a base class every module should extend the app library (App_Service_Base extends Tiger_Service_Service)

Rule of thumb, any time you're unsure: does it have a route? Route β†’ module. No route β†’ library.