-
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];
};