Skip to content
agovena.
agovena.
Get started
Community

Developer guides

Building Modules

Module manifests, discovery, registration contracts, dependencies, migrations, and lifecycle behavior.

On this page

A Module owns an optional domain

Use a Module when the feature introduces an optional commerce capability with domain rules and persistence. Digital delivery, events, domains, and provisioning are examples. Use an Extension when you are adapting a provider or contributing to an existing integration point. Use a Theme for presentation.

Core's public entrypoint is Modules/Contracts/Module.php:

php
interface Module
{
    public function id(): string;
    public function register(ModuleContext $context): void;
}

ModuleContext is deliberately independent of Livewire markup and CSS. It registers capabilities, permissions, navigation, account contributions, routes, and listeners. A Module can supply a Livewire screen, but its domain entrypoint does not have to be a screen component.

Scaffold in a development checkout

Run from Core with a valid sibling optional-packages directory configured:

dotenv
AGOVENA_OPTIONAL_PACKAGES_PATH=../optional-packages
bash
php artisan agovena:make-module workshop

The ID must match lowercase letters and numbers separated by single hyphens, beginning with a letter. The generator writes module.json, src/WorkshopModule.php, and src/WorkshopServiceProvider.php. It refuses an existing target unless --force is supplied. Use --force only if overwriting those scaffold files is intended.

Review the generated manifest before installation. The current generator writes agovena: "^0.1", while Core currently declares platform version 0.0.1 in config/agovena.php. A generated compatibility constraint is a starting point, not a guarantee it matches your checkout. Set the constraint to the versions you actually support and test.

Without a configured optional-packages root, the generator falls back to a Core modules/ folder. Runtime discovery does not automatically scan that fallback. Prefer the sibling repository, or explicitly configure the relevant extra_module_paths parent. Do not make first-party packages permanent Core subtrees.

Sources: MakeModuleCommand, ScaffoldingGenerator, and platform configuration.

Manifest contract

The Downloads manifest is a concrete example:

json
{
  "id": "downloads",
  "name": "Downloads",
  "version": "1.0.0",
  "description": "Downloadable file assets and customer entitlements after payment.",
  "author": "Agovena",
  "agovena": "^0.0.1",
  "group": "commerce",
  "provider": "Agovena\\Modules\\Digital\\DigitalServiceProvider",
  "dependencies": []
}
Field Runtime meaning
id, name, provider Required for discovery; provider is a PHP class name
version Package version, default 0.0.0
description Description, default empty
agovena Composer-style platform constraint, default *
dependencies List of other Module IDs, not version constraints
author Author label, default Agovena
group Catalog grouping, default other
autoload.psr-4 Optional namespace-to-relative-directory map

The runtime supplies the manifest's filesystem path; it is not a manifest path field. If PSR-4 is omitted, discovery derives the namespace from provider and uses src/ when present. For a custom namespace, declare it explicitly, for example "Acme\\Workshop\\": "src/". Namespace prefixes must end in a backslash; relative paths containing .. are ignored by the autoloader.

Sources: Availability capability reference, ModuleManifest, and PackageAutoload.

Provider and domain registration

A conventional provider extends Laravel ServiceProvider and exposes module(): Module. Laravel's register(): void binds services; the separate domain object's register(ModuleContext $context) registers Agovena contributions. Do not combine these incompatible method signatures in one class.

The Core availability provider binds the ProductStock contract and registers the physical availability listeners. Optional packages use the same contracts through explicit integration points; they do not provide inventory themselves:

php
$this->app->singleton(ProductStock::class, CatalogProductStock::class);

Event::listen(OrderPlacing::class, AssertStockBeforeOrderPlacing::class);
Event::listen(OrderCreated::class, ReserveStockWhenOrderCreated::class);
Event::listen(OrderCancelled::class, RestockWhenOrderCancelled::class);

This excerpt is from AvailabilityServiceProvider and AvailabilityCapability. Physical commerce consumes this shared Core availability contract; it is not an optional package.

Context methods and route boundaries

Context method Contribution
moduleId() Current package ID
admin() Admin registrar for permissions and UI contracts
capabilities() Product capability registry
customerAccountNav($item) Customer account navigation
customerAccountOverview($id, $factory, $sort) Customer-specific overview card
listen($event, $listener) Core or Module event subscription
adminRoutes($callback) /admin, name prefix admin., staff middleware
customerRoutes($callback) /account, name prefix customer., authenticated and verified customer middleware
apiRoutes($callback) /api/v1, name prefix api.v1., Sanctum, throttling, token abilities

A navigation permission is not a substitute for authorizing the underlying page or action. The Admin route helper supplies web authentication, permission synchronization, and admin.access; package actions still need their own relevant authorization checks.

Module API routes use the Core route-name-to-ability map. The supported prefixes are subscriptions., services., downloads., digital-secrets., and event-tickets. after api.v1.. A new arbitrary prefix fails bearer requests closed unless the Core mapping is extended deliberately. See ModuleContext and authentication.

Discovery and lifecycle

ModuleManager discovers installed-package storage first, then the optional repository, then extra roots. Each root contains child directories with module.json. The first matching ID wins.

  1. Discover: read manifests and register autoload mappings. Discovery does not enable the package.
  2. Install: validate platform compatibility and dependency discovery, create an installed-but-disabled record, and run package migrations. Installation does not automatically install dependencies.
  3. Enable: require installation and enabled dependencies, run pending migrations, persist enablement, register the provider, and call the Module contract once per manager runtime.
  4. Disable: mark the Module and dependent Modules disabled, and disable Extensions that depend on them. Preserve tables and records. Newly booted runtimes no longer register the disabled Module.
  5. Upgrade: migrateInstalled() applies pending migrations to installed Modules, including disabled ones.
  6. Uninstall: remove the installation marker. Data is not dropped. purgeData: true is rejected because generic purging is not implemented.

Keep package migrations in database/migrations. Use the supported manager and upgrade flow instead of assuming that toggling enablement or deleting a folder reverses schema changes.

Verify a Module change

Write tests for discovery, enabled capability registration, unavailable behavior while disabled, ownership, domain listeners, and preservation of data after disablement for optional packages. Core capabilities such as availability, physical commerce, and recurring billing are tested directly in Core and are not enabled through ModuleManager.

bash
php artisan test tests/Feature/ModuleRuntimeTest.php
php artisan test tests/Feature/ModuleMatrixTest.php

ModuleRuntimeTest verifies that Inventory stock lives outside products, is decremented during ordering, cannot become oversold in its tested scenario, and survives disablement. Follow contribution and testing for the full verification workflow.

Search documentation

Search guides, commands and API endpoints

What are you looking for?

Documentation