Module Basics

Structure your module, register it, and ship it with VelvetCMS.

Category: Modules

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 during module: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 #

  1. 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.
  2. Add it to storage/modules.json via velvet module:enable docs (creates the file if missing).
  3. Run velvet module:compile to validate dependencies and regenerate storage/modules-compiled.json + storage/modules-autoload.php.
  4. The next request/CLI boot loads modules in the resolved order, calling register() then boot().

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) using VersionRegistry::satisfies().
  • Checks for missing entry classes and duplicate names.
  • Resolves load order based on dependency graph.
  • Generates a Composer-style autoloader so only one spl_autoload_register is 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.php files.
  • Commands: resolve CommandRegistry inside boot() 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.