Skip to content

Module System

Ray Fung edited this page Feb 26, 2026 · 5 revisions

Module System

Modules are the fundamental building blocks of a Razy application. Each module is a self-contained package with its own controller, views, plugins, API commands, and assets.

Module Structure


sites/{distributor}/{vendor}/{module}/

→?→ module.php                     # REQUIRED: Module metadata

→?→ {version}/                     # e.g., "default", "1.0.0"

    →?→ package.php                # REQUIRED: Config & dependencies

    →?→ controller/

    →  →?→ {module_code}.php      # REQUIRED: Main controller

    →  →?→ {module_code}.main.php # Route handler

    →  →?→ {subfolder}/           # Nested route handlers

    →?→ view/                      # Template files (.tpl)

    →?→ plugins/                   # Custom plugins

    →?→ api/                       # API command handlers

    →?→ webassets/                 # Client-side assets

    →?→ src/                       # Library source code

🚨 Danger: Important: Modules go directly under sites/{dist}/{vendor}/{module}/ — do NOT create a modules/ subfolder.

module.php

Every module requires a module.php file at the module root:

<?php

return [

    'module_code' => 'vendor/module_name',

    'name'        => 'My Module',

    'author'      => 'Ray Fung',

    'description' => 'Module description',

    'version'     => '1.0.0',

];

package.php

Each version folder contains a package.php defining dependencies, API name, and prerequisites:

<?php

return [

    'api_name' => 'my_api',         // Optional API alias

    'require'  => [

        'vendor/other_module' => '*',  // Version constraint

    ],

    'prerequisite' => [

        'league/commonmark' => '^2.0',  // Composer package

    ],

];

require vs prerequisite: The require key declares dependencies on other Razy modules (resolved by the module loader at startup). The prerequisite key declares dependencies on Composer packages from Packagist or a private mirror (resolved by PrerequisiteResolver + PackageManager when you run php Razy.phar compose <distcode>). Packages are extracted into autoload/{distributor_code}/ with per-distributor isolation. See Packaging & Distribution for details.

Controller Pattern

Controllers use the anonymous class pattern, extending Razy\Controller:

<?php

namespace Razy\Module\my_module;



use Razy\Agent;

use Razy\Controller;



return new class extends Controller {

    public function __onInit(Agent $agent): bool

    {

        // Register routes

        $agent->addLazyRoute([

            '/'      => 'main',

            'action' => 'action',

        ]);



        // Register API commands

        $agent->addAPICommand('getData', 'api/get_data.php');



        // Listen to events

        $agent->listen('vendor/other:event', function($data) {

            return ['handled' => true];

        });



        return true;

    }



    public function __onReady(): void

    {

        // Safe to access APIs here

        // DI Container access via resolve()

        $service = $this->resolve(MyService::class);

    }

};

Route Handlers

Route handler files return a closure that receives captured URL parameters:

<?php

// controller/my_module.main.php

return function (): void {

    header('Content-Type: application/json');

    echo json_encode([

        'status' => 'ok',

        'module' => $this->getModuleInfo()->getCode(),

    ]);

};

Lifecycle Hooks

| Hook | Return | Purpose |

| --- | --- | --- |

| __onInit(Agent) | bool | Register routes, APIs, events. Return false to unload module. |

| __onLoad(Agent) | bool | Preload cross-module data. Return false to remove from queue. |

| __onDispatch() | bool | Before verifying module requirements. Return false to remove from queue. |

| __onRequire() | bool | Final validation. Return false to prevent execution. |

| __onReady() | void | Post-await setup. All APIs are safe to access. |

| __onRouted() | void | URL query matched this module's route. |

| __onEntry() | void | Route handler about to execute. Pre-handler logic. |

| __onDispose() | void | Cleanup after route/script completed. |

| __onAPICall() | bool | Authorize incoming API call. Return false to deny. |

Module Versioning

Modules can have multiple version folders. The distributor's dist.php selects which version to load:


'modules' => [

    '*'   => ['vendor/module' => 'default'],  // Default tag

    'dev' => ['vendor/module' => '0.1.0'],    // Dev tag

    'v2'  => ['vendor/module' => '2.0.0'],    // v2 tag

],

Shared Modules

Shared modules live in shared/module/ and are accessible to all distributors:


// dist.php →enable shared modules

'autoload_shared' => true,

'modules' => [

    '*' => [

        'shared/api-service' => 'default',

    ],

],

Module Status

Module lifecycle status is tracked via the ModuleStatus enum:

| Status | Value | Description |

| --- | --- | --- |

| Failed | -1 | Module failed to load |

| Disabled | 0 | Module is disabled |

| Unloaded | 1 | Module has not been loaded |

| Pending | 2 | Module is queued for loading |

| Initialing | 3 | Module is initializing |

| Processing | 4 | Module is processing |

| InQueue | 5 | Module is in the execution queue |

| Loaded | 6 | Module is fully loaded |

use Razy\Module\ModuleStatus;



$status = $module->getStatus();

if ($status === ModuleStatus::Loaded) {

    // Module is ready

}

Module Lifecycle Flow


┌─────────────────────────────────────────────────────────────────────────────────────────┐

│Unloaded →?→→→? Pending →?→→→? Initialing →?→→→? Processing →?→→→? InQueue →?→→→? Loaded │

└────────────────────┬──────────────┬────────────────┬────────────────────────────────────┘

                     │              │                │

                     │              │                │

┌────────────────────┴──────────────┴────────────────┴────────────────────────────────────┐

                │Failed  →   →Failed  →      →Failed  │

└─────────────────────────────────────────────────────────────────────────────────────────┘

Initialing__onInit() is called.

Processing — routes and event listeners are registered.

Loaded — module is ready to handle requests.

Common Mistakes

| Problem | Cause | Fix |

| --- | --- | --- |

| Module not found | module_code in module.php doesn't match folder path | Align module_code to vendor/module_name exactly |

| __onInit not called | Module status stuck at Pending | Ensure module.php returns a valid config array |

| Route never matches | Module loaded after routing phase | Check module status — must reach Loaded before requests |

| Shared module not visible | autoload_shared is false in dist.php | Set 'autoload_shared' => true or list the module explicitly |

← PreviousArchitecture

Next → Routing

Clone this wiki locally