Module Basics
Structure your module, register it, and ship it with VelvetCMS.
1. Manifest (module.json) #
Every module lives inside its own directory with a module.json declaring metadata:
{
"name": "docs",
"version": "1.0.0",
"entry": "VelvetCMS\\Docs\\DocsModule",
"description": "Adds the documentation UI",
"requires": {
"core": "^1.0",
"php": "^8.3"
}
}
Additional keys available:
provides,conflicts– dependency hints enforced duringmodule:compile.- Custom keys consumed by your module (VelvetCMS leaves them untouched).
Example – the bundled Docs module stores feature toggles under config:
{
"config": {
"versioning": true,
"base_path": "/docs"
}
}
Setting base_path to / mounts the module at the domain root (perfect for docs.example.com), while any other value becomes the leading URL segment.
2. Entry class #
Implement VelvetCMS\Contracts\Module or extend VelvetCMS\Core\BaseModule for helpers:
use VelvetCMS\Core\BaseModule;
use VelvetCMS\Core\Application;
final class DocsModule extends BaseModule
{
public function register(Application $app): void
{
$app->bind(DocsService::class, fn() => new DocsService($this->path()));
}
public function boot(Application $app): void
{
$router = $app->make('router');
require $this->path('routes/web.php');
}
}
BaseModule exposes $this->path('resources/views'), name(), version(), and manifest() so you can read metadata or locate assets.
3. Registering your module #
- Drop the module directory somewhere the loader can discover it:
modules/*inside the project.- Any glob listed in
config/modules.php(e.g.,../VelvetCMS-*). - Composer packages of type
velvetcms-module.
- Add it to
storage/modules.jsonviavelvet module:enable docs(creates the file if missing). - Run
velvet module:compileto validate dependencies and regeneratestorage/modules-compiled.json+storage/modules-autoload.php. - The next request/CLI boot loads modules in the resolved order, calling
register()thenboot().
4. Discovery + validation #
ModuleManager::discover() merges modules from config, filesystem, and Composer. module:compile then:
- Skips disabled modules early.
- Validates
requires(core/php/other modules) usingVersionRegistry::satisfies(). - Checks for missing
entryclasses and duplicate names. - Resolves load order based on dependency graph.
- Generates a Composer-style autoloader so only one
spl_autoload_registeris needed, even for 100+ modules.
5. Shipping features from register() vs boot() #
| Phase | Ideal tasks |
|---|---|
register() |
Bind services into the container, merge config values, publish singletons, alias facades. No heavy work—this runs for every CLI/HTTP entrypoint. |
boot() |
Access other services, register routes, add CLI commands, hook into events, add theme/template paths, modify cache behavior. |
6. Integrations to consider #
- Routing: add closures/controllers via the router resolved from
$app->make('router'). - Theme templates: call
$app->make('theme')->addPath($this->path('theme'))so your module can ship.velvet.phpfiles. - Commands: resolve
CommandRegistryinsideboot()and register custom CLI tooling. - Events: subscribe to
router.*,page.*,markdown.*,exception.*to extend the lifecycle without monkey patching core.
Once compiled, modules behave like first-class citizens and survive upgrades because they only depend on public contracts.