Modules
Module manifests, auto-loading conventions, and tenant-aware module tooling.
Modules are the main extension mechanism in VelvetCMS Core. A module can contribute services, routes, views, public assets, commands, and config without changing Core itself.
Module Structure #
Blog/
├── module.json
├── src/
│ └── BlogModule.php
├── config/
│ └── editor.php
├── routes/
│ ├── web.php
│ └── api.php
├── resources/
│ └── views/
│ └── dashboard.velvet.php
└── public/
└── app.css
Only module.json and a valid entry class are required. The other directories are conventions Core understands automatically.
Module Manifest #
Every module needs a module.json file. The manifest lists dependencies, optional routes, views, commands, and other metadata parsed by ModuleManifest.
{
"name": "blog",
"version": "1.0.0",
"entry": "Vendor\\Blog\\BlogModule",
"description": "Blog tools for VelvetCMS Core",
"stability": "stable",
"requires": {
"core": "^2.4"
},
"commands": {
"blog:reindex": "Vendor\\Blog\\Commands\\ReindexCommand"
},
"routes": {
"web": "routes/web.php"
},
"views": "resources/views"
}
Manifest Fields #
| Field | Required | Description |
|---|---|---|
name |
Yes | Unique module identifier |
entry |
Yes | Fully qualified class name of the module entry class |
version |
Yes | Module version string |
description |
No | Human-readable description |
stability |
No | stable, beta, rc, and so on |
requires |
No | Dependencies as name → semver constraint (core, php, other modules) |
conflicts |
No | Modules that cannot coexist with this package |
provides |
No | Capability tags |
commands |
No | Map of CLI signature → command class FQCN |
routes |
No | { "web": "routes/web.php", "api": "routes/api.php" } style map |
views |
No | Relative path registered as <module-name>:: view namespace |
Only keys understood by ModuleManifest survive compilation verbatim. Arbitrary authoring metadata belongs in extra:
"extra": {
"marketing": {
"homepage": "https://example.com"
}
}
Dependency Constraints #
Constraints are parsed with composer/semver, so you can use standard Composer-style version ranges:
>=1.0.0- minimum version<2.0.0- maximum version>=1.0.0 <2.0.0- range^2.4- compatible within Core major series2.*~2.4.0- compatible within Core minor line2.4.*
Entry Class #
The manifest entry class is instantiated by the module manager and must implement the module contract used by Core. In practice, most modules use the base module support from Core rather than implementing everything from scratch.
The entry class is where you keep module-specific service bindings or event listeners that do not fit the built-in conventions.
namespace Vendor\Blog;
use VelvetCMS\Core\Application;
use VelvetCMS\Core\BaseModule;
final class BlogModule extends BaseModule
{
public function register(Application $app): void
{
$app->singleton('blog.search', fn () => new SearchIndex(storage_path('blog')));
}
public function boot(Application $app): void
{
$app->get('events')->listen('page.saved', function ($page) {
// React to page changes.
});
}
}
Discovery #
Modules are discovered from multiple sources:
config/modules.phpexplicit module entries- filesystem paths and glob patterns from
modules.paths - Composer packages with the
velvetcms-modulepackage type - tenant-specific paths from
modules.tenant_pathswhen tenancy is enabled
See Modules Configuration for the scan paths and tenant-specific discovery rules.
Convention-Based Auto-Loading #
VelvetCMS Core 2.x progressively reduced boilerplate modules need in boot(). The conventions below stabilized early in 2.1 and still apply today:
Config #
If a module contains config/*.php, those files are registered automatically under the module namespace.
config('blog:editor.enabled');
config('blog:editor.toolbar');
The separator stabilized in 2.1. Module config uses namespace:file.key, not namespace.file.key.
Historically modules merged config arrays by hand inside register(). Core auto-registers config/*.php under the module namespace and mergeConfigFrom() was removed accordingly.
Views #
If resources/views/ exists, Core exposes it automatically as a namespaced view path:
echo view('blog::dashboard', ['stats' => $stats]);
Routes #
If routes/web.php or routes/api.php exist, Core requires them automatically during module boot.
To opt out, disable route auto-loading in the manifest:
{
"autoload": {
"routes": false
}
}
Commands #
Commands can be declared directly in the manifest instead of being registered imperatively through events:
{
"commands": {
"blog:reindex": "Vendor\\Blog\\Commands\\ReindexCommand"
}
}
Core registers those signatures during the commands.registering phase.
Compiled Manifest #
Module discovery is compiled into tenant-aware artifacts for faster startup. The exact file locations depend on tenancy, but the workflow is consistent:
./velvet module:compilevalidates manifests and writes compiled metadata../velvet module:provisionmigrates global module artifacts into tenant scope when needed.
Compiled output contains the resolved module list, dependency order, and generated autoload metadata. Runtime loading reads those artifacts instead of re-scanning every module directory on every request.
Load Order #
During compilation and boot, the module manager:
- collects all discovered modules
- validates manifests and version constraints
- filters disabled modules
- sorts modules by dependency order
- registers modules and then boots them in order
If module A requires module B, Core will load module B first.
Tooling #
For day-to-day module work, the key commands are:
module:compileto validate manifests and build runtime artifactsmodule:enableandmodule:disableto manage activation statemodule:provisionto migrate global module artifacts into tenant scope when neededmake:moduleto scaffold a new module
The full command reference lives in Command Reference.