Submodules
Modules can nest infinitely. URL routing, assets, and frames cascade down the tree automatically.
What Are Submodules?
A submodule is a full module that lives inside another module's submodule/ directory. It has its own controller, views, assets, and models — but inherits its parent's frame and routing context. This lets you break large features into organized pieces without cluttering the top-level modules/ directory.
Directory Structure
Submodules live in a submodule/ directory inside their parent:
Each submodule is a self-contained module with the same structure as a top-level module — controller, views, assets, and models are all supported.
Namespace Convention
A submodule's namespace is its parent module name, not its own. This is how the framework's autoloader resolves submodule classes:
<?php
namespace Zero\Module\Ttrpg;
class Characters extends \Zero\Core\Module {
public function index() {
$this->respond($this->viewPath . 'index.php');
}
public function view($id) {
$this->data = ['character_id' => $id];
$this->respond($this->viewPath . 'view.php');
}
}
The namespace for Characters is Zero\Module\Ttrpg, not Zero\Module\Characters. This is required for the autoloader to find the class when routing delegates to the submodule.
Compare this to the parent module:
<?php
namespace Zero\Module;
class Ttrpg extends \Zero\Core\Module {
public function index() {
$this->respond($this->viewPath . 'index.php');
}
// No characters() method needed - the framework
// automatically delegates to the submodule
}
URL Routing Delegation
When a URL comes in, the framework first tries to call the matching method on the parent module. If the method doesn't exist, it checks for a matching submodule via Module::__call():
- URL
/ttrpg/characters/view/5arrives - Framework loads
Ttrpgmodule and callscharacters('view', '5') Ttrpghas nocharacters()method —__call()fires- Framework finds
submodule/Characters/Characters.php - Routing shifts:
Characters::view('5')is called
| URL | Resolution |
|---|---|
/ttrpg |
Ttrpg::index() |
/ttrpg/characters |
Characters::index() (submodule) |
/ttrpg/characters/view/5 |
Characters::view('5') (submodule) |
/ttrpg/hexmap/dm |
Hexmap::dm() (submodule) |
If the parent module has a characters() method, it takes precedence over the submodule. The submodule is only checked when no matching method exists on the parent.
Asset Loading for Submodules
Submodule assets are fully supported by the asset system. They are symlinked under the parent module's asset directory:
Auto-loading follows the same convention, scoped to the submodule. For the URL /ttrpg/hexmap/dm, the asset loader checks /assets/ttrpg/hexmap/css/ for:
| File | Loaded Because |
|---|---|
hexmap.css |
Matches the submodule name (loads for all Hexmap endpoints) |
dm.css |
Matches the endpoint name (loads only for /ttrpg/hexmap/dm) |
The same $this->getStylesheets() and $this->getScripts() calls work in submodule frames — the methods automatically resolve to the correct submodule asset path.
If a submodule needs assets that don't follow the naming convention (like main.css or a vendor library), hardcode them in the submodule's frame/head.php alongside the $this->getStylesheets() call.
Frame Inheritance
Submodules inherit their parent's frame templates (head, header, footer, sideNav) by default. The framework walks up the directory tree looking for frame files:
- Check submodule:
modules/Ttrpg/submodule/Characters/frame/ - Check parent module:
modules/Ttrpg/frame/ - Fall back to app frame:
app/frontend/frame/
This means a submodule gets the parent's header and navigation for free. Override any individual frame file by creating it in the submodule's own frame/ directory — only that file is overridden, the rest continue to inherit.
Infinite Nesting
Submodules can contain their own submodules, creating an arbitrarily deep hierarchy:
The URL /ttrpg/characters/inventory/view/42 would resolve to Inventory::view('42') through two levels of delegation. Each level follows the same namespace and routing rules.
Submodules work best when features are logically children of a parent (Characters belong to a TTRPG app). If two modules are peers that share nothing, keep them as top-level modules instead.