Module anatomy and module.json
The folder layout, every field of the manifest, and the lifecycle methods the kernel calls.
A module is a self-contained directory, loadable at runtime and installable from a zip. The factory modules use exactly this contract.
Folder layout#
modules/directory/
module.json manifest (required)
src/
DirectoryServiceProvider.php
Models/
Http/Controllers/
Http/Requests/
Support/
database/
migrations/ the module tables
seeders/
resources/
views/ Blade, overridable by a theme
lang/ the module translations
assets/ css and js, ALREADY COMPILED
routes/
web.php
admin.php
config/directory.php
The manifest#
{
"name": "Directory",
"slug": "directory",
"version": "1.2.0",
"description": "A configurable catalogue.",
"author": { "name": "You", "url": "https://example.com" },
"requires": {
"bondry": ">=1.0 <2.0",
"php": ">=8.2",
"extensions": ["gd"],
"modules": { "payments": ">=1.0" }
},
"provider": "Modules\\Directory\\DirectoryServiceProvider",
"provides": {
"permissions": ["directory.view", "directory.manage"],
"hooks": ["profile.tabs", "admin.menu", "search.sources"]
},
"settings": "admin.directory.settings",
"assets": { "css": ["assets/directory.css"], "js": ["assets/directory.js"] },
"parent": null,
"tested_up_to": "1.4.0",
"official": false,
"licensing": { "product_slug": "your-directory" }
}
| Field | Meaning |
|---|---|
slug |
The identity. [a-z0-9-], unique, and it never changes between versions. |
version |
Semver. It is what drives onUpdate. |
requires |
Semver ranges. The kernel refuses to install when they are not met. |
provider |
The root class. It has to exist and extend the base provider. |
provides.permissions |
Registered into the group matrix on install. |
settings |
A route name. The module list links to it when the module is enabled. |
parent |
The slug of the module this one extends. Absent means it extends the core. |
tested_up_to |
The newest core version you tested against. |
official |
Only accepted for packages signed by us. |
licensing.product_slug |
Present only on a paid module. See License API. |
The service provider#
Your provider extends Bondry\Kernel\Modules\ModuleServiceProvider and
implements the lifecycle:
interface ModuleContract
{
// Every request, only while the module is enabled:
public function register(): void; // container bindings
public function boot(): void; // routes, views, migrations, translations, hooks
// Called by the runtime installer, not on every request:
public function onInstall(): void;
public function onUninstall(bool $purge): void;
public function onEnable(): void;
public function onDisable(): void;
public function onUpdate(string $from, string $to): void;
}
boot() is where you call loadRoutesFrom, loadViewsFrom with your
namespace, loadMigrationsFrom, loadTranslationsFrom and register your hooks.
register() runs before anything is resolved: bindings only, no database.
Warning
boot()runs on every request. Anything expensive there is a cost your buyers pay on every page view, on shared hosting.
Views and the theme#
Register your views with a namespace:
$this->loadViewsFrom(__DIR__.'/../resources/views', 'directory');
Rendering directory::item.show then resolves through the theme chain first,
so a buyer can override any screen of your module from their theme without
editing your files. See Themes.
Install flow#
What the panel does with your zip, in order:
- opens it in an isolated temporary directory;
- reads
module.jsonand validates the schema; - checks
requiresand any slug or version conflict; - verifies the signature when
officialis set; - moves it to
modules/<slug>/and runsonInstall(), oronUpdate()when a version was already there; - publishes assets to
public/modules/<slug>/; - records it in the modules table;
- clears the route, view and config caches.