-
Notifications
You must be signed in to change notification settings - Fork 0
Module Structure
Understanding how Razy modules are organized on disk.
Every Razy module follows a strict directory convention. The framework uses this structure to auto-discover controllers, views, API handlers, web assets, and plugins. Modules live directly under the distributor's own folder (e.g., sites/{dist}/) or the global shared/module/ folder.
New to Razy? See the Hello World demo module ??a 4-file minimal module with only one route and plain text output. Every file is commented explaining why each line exists.
{vendor}/my-module/
?œâ??€ module.php # Module metadata (required)
?œâ??€ default/ # Default version folder (required)
?? ?œâ??€ package.php # Package configuration (required)
?? ?œâ??€ controller/ # Controller closures
?? ?? ?œâ??€ my_module.php # Main controller (class name match)
?? ?? ?œâ??€ my_module.index.php # Route handler "index"
?? ?? ?œâ??€ my_module.detail.php # Route handler "detail"
?? ?? ?”â??€ api/ # API command handlers
?? ?? ?œâ??€ getData.php # API command "getData"
?? ?? ?”â??€ submit.php # API command "submit"
?? ?œâ??€ view/ # Template files
?? ?? ?œâ??€ main.tpl # Main template
?? ?? ?œâ??€ list.tpl # List template
?? ?? ?”â??€ include/ # Included partials
?? ?? ?”â??€ alert.tpl
?? ?œâ??€ webassets/ # Web-accessible static files
?? ?? ?œâ??€ css/
?? ?? ?œâ??€ js/
?? ?? ?”â??€ img/
?? ?”â??€ plugins/ # Plugin closures
?? ?”â??€ my_plugin.php
?”â??€ 1.0.0/ # Tagged version (optional)
?œâ??€ package.php
?œâ??€ controller/
?”â??€ view/
The root-level module.php defines module identity. It sits outside any version folder and provides metadata used by the distributor.
return [
'module_code' => 'vendor/my-module',
'name' => 'My Module',
'author' => 'Author Name',
'description' => 'Module description',
'version' => '1.0.0',
];The module_code must be in vendor/package format, matching the directory path.
Each version folder contains a package.php that defines what gets loaded at runtime:
return [
'module_code' => 'vendor/my-module',
'author' => 'Author Name',
'description' => 'Package description',
'version' => '1.0.0',
'api_name' => 'mymod', // Cross-module API identifier
'alias' => 'my_module', // URL alias (defaults to class name)
'shadow_asset' => false, // Symlink webassets instead of copying
'require' => [
'vendor/other' => '>=1.0.0',
],
];| Key | Type | Description |
|---|---|---|
module_code |
string |
Module identifier in vendor/package format |
author |
string |
Author name |
description |
string |
Package description |
version |
string |
Semantic version |
api_name |
string |
Cross-module API identifier for $this->api('name')
|
alias |
string |
URL alias used in webassets and route paths. Defaults to the controller class name if not set. |
shadow_asset |
bool |
When true, webassets are symlinked instead of copied during deployment. Automatically disabled for PHAR-packaged modules. Default: false. |
require |
array |
Module dependencies with semver constraints |
The controller/ directory contains PHP closure files that the framework auto-loads. Files are named following a strict convention:
| File Pattern | Purpose | Example |
|---|---|---|
{class}.php |
Main controller ??extends Controller, defines lifecycle hooks |
my_module.php |
{class}.{name}.php |
Route handler closure ??registered via addRoute() / addLazyRoute()
|
my_module.index.php |
api/{command}.php |
API command handler ??registered via addAPICommand()
|
api/getData.php |
demo/{name}.php |
Custom sub-folders for logical grouping | demo/overview.php |
The main controller file returns an anonymous class extending Controller:
return new class extends Controller {
protected function __onInit(Agent $agent): bool
{
$agent->addRoute('/', 'index');
$agent->addAPICommand('getData', 'api/getData');
return true;
}
};Route handler files return a Closure that receives captured URL parameters:
// controller/my_module.index.php
return function () {
$template = $this->loadTemplate('main');
echo $template->output();
};The view/ folder holds .tpl template files used by the Razy Template Engine. Templates are loaded via $this->loadTemplate('name') in the controller, which resolves to view/name.tpl.
You can organize templates into subdirectories. The include/ subfolder is a convention for partial templates referenced by the INCLUDE block type.
The webassets/ directory contains publicly accessible static files (CSS, JS, images). These are served directly by the web server via Apache rewrite rules generated by php Razy.phar rewrite.
The URL structure is: {siteURL}/webassets/{moduleAlias}/{version}/{file}
Use $this->getAssetPath() in your controller to get the base URL, then append the file path:
// In a route handler closure
$cssUrl = $this->getAssetPath() . 'css/style.css';
// ??https://example.com/webassets/my_module/default/css/style.cssThe plugins/ directory contains plugin closure files that extend the Template Engine or Collection system. Plugins are loaded via $this->registerPluginLoader() in the controller. See the Plugin System page for details.
Modules support multiple versions via named version folders:
{vendor}/my-module/
?œâ??€ module.php
?œâ??€ default/ ??default version
?? ?œâ??€ package.php
?? ?”â??€ ...
?œâ??€ 1.0.0/ ??tagged version
?? ?œâ??€ package.php
?? ?”â??€ ...
?”â??€ 2.0.0/ ??another tagged version
?œâ??€ package.php
?”â??€ ...
The dist.php configuration selects which version to load:
'modules' => [
'*' => [
'vendor/my-module' => '*', // Latest version
'vendor/my-module' => 'default', // Default folder
'vendor/my-module' => '1.0.0', // Specific tag
],
],See Packaging & Distribution for full version management details.
| Location | Type | Description |
|---|---|---|
shared/module/ |
Shared | Available to all distributors. Enabled via global_module or autoload_shared in dist.php
|
sites/{dist}/ |
Distributor-specific | Only available to that distributor. Modules go directly under the site folder as {vendor}/{module}/
|
Custom module_path
|
Override | Set module_path in dist.php to use an alternative module source directory |
demo_modules/core/route_demo/
?œâ??€ module.php
?”â??€ default/
?œâ??€ package.php
?œâ??€ controller/
?? ?œâ??€ route_demo.php # Main controller
?? ?œâ??€ route_demo.main.php # /route handler
?? ?œâ??€ route_demo.article.php # /article/:id handler
?? ?œâ??€ route_demo.product.php # /product/:slug handler
?? ?œâ??€ route_demo.search.php # /search handler
?? ?œâ??€ route_demo.user.php # /user/:id handler
?? ?œâ??€ route_demo.tag.php # /tag/:name handler
?? ?”â??€ route_demo.code.php # /code/:lang handler
?”â??€ view/
?”â??€ main.tpl
Each .{name}.php file corresponds to a route action registered in the main controller's __onInit via $agent->addRoute().
Here's a full, working module you can copy as a starting point:
// myteam/blog/module.php
return [
'module_code' => 'myteam/blog',
'name' => 'Blog',
'author' => 'My Team',
'description' => 'A blog module with listing and detail views',
'version' => '1.0.0',
];// myteam/blog/default/package.php
return [
'module_code' => 'myteam/blog',
'author' => 'My Team',
'description' => 'Blog module ??default version',
'version' => '1.0.0',
'api_name' => 'blog',
'alias' => 'blog',
'require' => [],
];// myteam/blog/default/controller/blog.php ??Main controller
use Razy\Controller;
use Razy\Agent;
return new class extends Controller {
protected function __onInit(Agent $agent): bool
{
// Routes
$agent->addRoute('/', 'index');
$agent->addRoute('/post/(:d)', 'post'); // :d = digits
$agent->addRoute('/search/(:a)', 'search'); // :a = any
// API commands (callable from other modules)
$agent->addAPICommand('getPost', 'api/getPost');
// Listen for events
$agent->listen('onReady', 'events/ready');
return true;
}
protected function __onReady(): void
{
// All modules loaded ??perform late initialization
}
};// myteam/blog/default/controller/blog.index.php ??List route
return function () {
$template = $this->loadTemplate('list');
$template->assign('title', 'Blog Posts');
echo $template->output();
};// myteam/blog/default/controller/blog.post.php ??Detail route
return function (string $postId) {
$db = $this->resolve(\Razy\Database::class);
$post = $db->prepare()
->select('*')
->from('posts')
->where('id=:id')
->lazy(['id' => $postId]);
if (!$post) {
throw new \Razy\Exception\NotFoundException('Post not found');
}
$template = $this->loadTemplate('detail');
$template->assign('post', $post);
echo $template->output();
};// myteam/blog/default/controller/api/getPost.php ??API handler
return function () {
$params = func_get_args();
$id = $params[0]['id'] ?? null;
if (!$id) {
return ['error' => 'Missing post ID'];
}
$db = $this->resolve(\Razy\Database::class);
$post = $db->prepare()
->select('id, title, content')
->from('posts')
->where('id=:id')
->lazy(['id' => $id]);
return ['post' => $post];
};