stave.dev

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:

modules/ MyModule/ MyModule.php ← Controller class view/ ← View templates index.php edit.php model/ ← Database models (auto-loaded) MyModel.php assets/ ← CSS, JS, components css/ my-module.css ← Loaded for all endpoints edit.css ← Loaded only for /my-module/edit js/ my-module.js component/ ← ShadowComponent HTML files frame/ ← Optional layout overrides head.php header.php submodule/ ← Nested submodules config.ini ← Optional module-specific config

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');
    }
}
Namespace Convention

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/
Use the Path Properties

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:

  1. Create the module directorymodules/MyModule/
  2. Create the controllermodules/MyModule/MyModule.php extending \Zero\Core\Module
  3. Add an index() method — This is the default endpoint when no method is specified in the URL
  4. Create a view directorymodules/MyModule/view/ with at least index.php
  5. Add CSS (optional) — modules/MyModule/assets/css/my-module.css
  6. Add models (optional) — modules/MyModule/model/ with the Zero\Module\MyModule\Model namespace
  7. Add frame overrides (optional) — modules/MyModule/frame/ for custom layouts
Class Name Must Match Directory

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:

  1. Converts the kebab-case URL to your PascalCase class name
  2. Loads and instantiates your module class
  3. Creates symlinks for your assets/ directory to the public web root
  4. Resolves viewPath, framePath, componentPath, and modelPath
  5. Registers autoloaders for your model/ directory
  6. Checks PHP attributes on the class and method
  7. Calls the endpoint method with URL arguments unpacked via the splat operator
  8. Auto-loads matching CSS and JS files from your assets/ directory
  9. Wraps the response in the frame (head, header, footer, sideNav)