Routing & pretty URLs

Tiger's routing has one liberating default: you rarely need to write a route at all. Every module surface is already reachable, and pretty URLs are declared, not hand-registered.

Convention first — the canonical path is free

Every controller/action is reachable at its canonical MVC path via ZF1's built-in :module/:controller/:action route, with zero registration:

Docs_IndexController::docsAction       →  /docs/index/docs
Billing_InvoiceController::viewAction  →  /billing/invoice/view/id/42

Trailing key/value pairs fold into params automatically (/billing/invoice/view/id/42id=42). Do not register a route for this — it exists for free, it's stable, and it's what everything else points at.

No query strings for navigation. Use path-style params — /auth/login/out/1, not ?out=1. A return-destination path lives in the session, never the URL (avoids %2F 404s and history leakage).

Pretty routes are overrides, not addRoute

A vanity URL like /docs (instead of /docs/index/docs) is an optional alias. You declare it — you don't imperatively $router->addRoute() in a module Bootstrap. Declaring puts every alias through one ordering authority; hand-adding scatters precedence across bootstraps and makes you fight Zend's route stack.

class Docs_Bootstrap extends Zend_Application_Module_Bootstrap
{
    protected function _initRouteOverride()
    {
        Tiger_Routing_Overrides::register('docs', [
            'pattern'  => 'docs',            // public prefix; the remainder arrives as `slug`
            'target'   => 'docs/index/docs', // the canonical module/controller/action it maps to
            'priority' => 100,               // higher = checked first when prefixes overlap
        ]);
    }
}

That's the whole contract: a public prefix maps to a canonical target, and the remaining path arrives as slug. /docs → the target with no slug; /docs/guides/deploy → the target with slug = guides/deploy.

The admin can retarget it live — no deploy

Overrides live in the config DB tier (tiger.routing.override.<name>.*), so any field is overridable at runtime, effective next request:

tiger.routing.override.docs.pattern  = "help"   ; serve the docs at /help instead
tiger.routing.override.docs.enabled  = 0        ; turn the pretty route off (canonical path still works)
tiger.routing.override.docs.priority = 250      ; reorder against another module's alias

Same live-override pattern as everything else — a settings screen writes these keys via Tiger_Model_Config. No new table, no deploy.

How it resolves (and why order stops mattering)

A plugin runs at routeShutdown (after routing, before dispatch). For each URL:

  1. A real controller claims it? It wins — canonical MVC paths, /api, /auth, /admin, and a module's own controllers. Overrides never shadow them. (This is why the reserved prefixes are safe, and why /docs never hides /docs/admin/settings.)
  2. A declared override prefix matches? It's rewritten to the canonical target (highest priority first).
  3. A published CMS page / redirect? Served (or 301'd).
  4. Nothing? A clean 404.

Because the plugin decides order itself, overrides are immune to Zend's last-in-first-out route matching — you never wrestle the route stack for a module alias.

The rules of thumb

  • Reach features at <module>/<controller>/<action>; don't register the canonical path.
  • Want a pretty URL? Declare an override; never addRoute an alias in a module Bootstrap.
  • A module never touches web-server config. Pretty routes are PHP-layer overrides that work the instant a module activates — editing Apache/nginx to route a module would break 1-click install. (Changing your own deployment's web server is your prerogative — that's separate from installing a module.)
  • Reserved prefixes api, auth, admin can never be claimed by an override.
  • Priority is open — a module may declare any weight. Keep defaults sane (≈100) so an admin rarely has to reorder.