Creating Modules
Self-contained application units with controllers, views, models, and assets — everything a feature needs in one directory.
Module Directory Structure
A module is a directory inside modules/ containing a PHP controller class of the same name. Everything the module needs lives alongside it:
Controller Class
The controller is the only required file. It must extend \Zero\Core\Module and live in the Zero\Module namespace:
<?php
namespace Zero\Module;
class MyModule extends \Zero\Core\Module {
public function index() {
$this->data = ['title' => 'My Module'];
$this->respond($this->viewPath . 'index.php');
}
public function edit($id) {
$this->data = ['id' => $id];
$this->respond($this->viewPath . 'edit.php');
}
}
All top-level module controllers use namespace Zero\Module. Submodule controllers extend this with the parent name: namespace Zero\Module\ParentModule.
Auto-Defined Paths
When a module is instantiated, the framework automatically resolves these path properties based on the module's directory location. If a directory exists inside the module, it gets the module-local path; otherwise, it falls back to the global default.
| Property | Module Path | Fallback Path |
|---|---|---|
$this->viewPath |
modules/MyModule/view/ |
app/frontend/view/ |
$this->framePath |
modules/MyModule/frame/ |
app/frontend/frame/ |
$this->componentPath |
modules/MyModule/component/ |
app/frontend/component/ |
$this->modelPath |
modules/MyModule/model/ |
app/frontend/model/ |
Always reference views with $this->viewPath . 'index.php' instead of hardcoding paths. This ensures your module works correctly even when used as a submodule with inherited paths.
Asset Auto-Loading
Module assets in modules/MyModule/assets/ are automatically symlinked to the public web root on first request. CSS and JS files are then auto-loaded based on naming convention:
| File Name | Loaded When |
|---|---|
css/my-module.css |
Every endpoint in the module |
css/edit.css |
Only the /my-module/edit endpoint |
js/my-module.js |
Every endpoint in the module |
js/edit.js |
Only the /my-module/edit endpoint |
Asset file names use kebab-case to match URL format, not PascalCase class names.
Complete Example
Here is a complete CRUD-like module that manages articles with both HTML views and JSON API support:
<?php
namespace Zero\Module;
use Zero\Core\Database;
use Zero\Core\Request;
use Zero\Core\HTTPError;
use Zero\Core\Attribute\AllowedMethods;
use Zero\Core\Attribute\RequiredParams;
use Zero\Core\Attribute\Sanitize;
use Zero\Core\Attribute\RequireLogin;
use Zero\Module\Article\Model\ArticleModel;
class Article extends \Zero\Core\Module {
private ArticleModel $model;
public function __construct() {
parent::__construct();
$db = Database::getConnection();
$this->model = new ArticleModel($db);
}
/**
* List all articles
* GET /article
*/
public function index() {
$this->data = ['articles' => $this->model->getAll()];
$this->respond($this->viewPath . 'index.php');
}
/**
* Show a single article
* GET /article/show/42
*/
public function show($id) {
$article = $this->model->find($id);
if (!$article) {
throw new HTTPError(404, 'Article not found');
}
$this->data = ['article' => $article];
$this->respond($this->viewPath . 'show.php');
}
/**
* Create a new article
* POST /article/create
*/
#[RequireLogin]
#[AllowedMethods('POST')]
#[RequiredParams(['title', 'content'])]
#[Sanitize(['title', 'content'])]
public function create() {
$id = $this->model->create([
'title' => $_POST['title'],
'content' => $_POST['content'],
'user_id' => $_SESSION['user_id'],
]);
$this->success('Article created', ['id' => $id]);
header('Location: /article/show/' . $id);
}
/**
* Delete an article
* POST /article/delete/42
*/
#[RequireLogin]
#[AllowedMethods('POST')]
public function delete($id) {
$this->model->delete($id);
$this->success('Article deleted');
header('Location: /article');
}
}
Module Checklist
When creating a new module, follow these steps:
- Create the module directory —
modules/MyModule/ - Create the controller —
modules/MyModule/MyModule.phpextending\Zero\Core\Module - Add an
index()method — This is the default endpoint when no method is specified in the URL - Create a view directory —
modules/MyModule/view/with at leastindex.php - Add CSS (optional) —
modules/MyModule/assets/css/my-module.css - Add models (optional) —
modules/MyModule/model/with theZero\Module\MyModule\Modelnamespace - Add frame overrides (optional) —
modules/MyModule/frame/for custom layouts
The controller class name must match the directory name exactly. A module in modules/UserProfile/ requires a class named UserProfile in UserProfile.php. The framework locates modules using this convention — there is no routing configuration to set up.
What the Framework Handles
When a request comes in for your module, the framework automatically:
- Converts the kebab-case URL to your PascalCase class name
- Loads and instantiates your module class
- Creates symlinks for your
assets/directory to the public web root - Resolves
viewPath,framePath,componentPath, andmodelPath - Registers autoloaders for your
model/directory - Checks PHP attributes on the class and method
- Calls the endpoint method with URL arguments unpacked via the splat operator
- Auto-loads matching CSS and JS files from your
assets/directory - Wraps the response in the frame (head, header, footer, sideNav)