Manifest Reference
plugin.json is the only required configuration file. It tells the framework where the entry class
lives, what it needs, and what it contributes.
There is no module.json. Menus, legacy Blade hooks, and page-builder addons live inside
xilancerMetaData.
Complete example
{
"name": "My Plugin",
"slug": "my-plugin",
"version": "1.0.0",
"alias": "myplugin",
"entry": {
"file": "src/MyPlugin.php",
"class": "MyPlugin\\MyPlugin"
},
"autoload": {
"psr-4": {
"MyPlugin\\": "src/"
}
},
"requires": {
"framework": ">=1.0",
"php": ">=8.3",
"plugins": {
"referrals": ">=1.0"
}
},
"provides": [
"checkout.price.calculate",
"dashboard.referral_panel",
"admin.menu"
],
"capabilities": [
"outbound-https"
],
"legacyName": "MyPlugin",
"xilancerMetaData": {
"plugin_type": "external",
"hooks": {
"body_end": [
"assets.banner"
]
},
"admin_settings": {
"menu_item": [
{
"id": "my-plugin-menu",
"label": "My Plugin",
"route": "admin.my-plugin.index",
"parent": null,
"icon": "fa-solid fa-star",
"permissions": "my-plugin-manage",
"class": "bg-success"
}
]
}
}
}
Required fields
| Field | Type | Rules |
|---|---|---|
name | string | Non-empty. Display name. |
slug | string | Lowercase letters, numbers, and dashes. Must start with a letter or number. Matches the directory intent. |
version | string | Numeric dotted, up to four segments: 1, 1.2, 1.2.3, 1.2.3.4. |
entry.file | string | Path relative to the plugin directory. Must exist. |
entry.class | string | Fully qualified class name. Must implement PluginsFramework\Plugins\PluginInterface. |
The validator reports the first failure and stops. Fix it before continuing.
Entry
"entry": {
"file": "src/MyPlugin.php",
"class": "MyPlugin\\MyPlugin"
}
- The file is loaded with
require_once. - The class is instantiated as
new $class($context). - The constructor must accept
PluginContextas its first parameter. boot()is called once per request (or per CLI command).
The class may have other constructor parameters only if they have defaults; the manager passes one argument.
Autoload
"autoload": {
"psr-4": {
"MyPlugin\\": "src/",
"MyPlugin\\Support\\": "support/"
}
}
- Each namespace maps to a directory relative to the plugin root.
- The bridge registers these on the Composer loader before boot.
- Use a trailing backslash in the namespace key.
- Do not commit a
vendor/directory unless you truly need one. Prefer zero dependencies.
If you need a file outside PSR-4, require_once it from the entry file. Keep that list short.
Requires
"requires": {
"framework": ">=1.0",
"php": ">=8.3",
"plugins": {
"referrals": ">=1.0",
"cloud-storage": "^1.2"
}
}
Supported constraints
| Form | Meaning |
|---|---|
* or empty | Any version |
1.2.3 | Exactly 1.2.3 |
=1.2.3 | Exactly 1.2.3 |
>=1.2 | At least 1.2 |
>1.2 | Greater than 1.2 |
<=2.0 | At most 2.0 |
<2.0 | Less than 2.0 |
^1.2 | At least 1.2, below 2.0 |
>=1.0 <2.0 | Both conditions (space or comma separated) |
The implementation is a small semver checker, not Composer. It does not support ~, ||, or
wildcards other than *.
Framework version
framework is checked against PluginFramework::VERSION, currently 1.0.0.
PHP version
php is checked against PHP_VERSION. Xilancer 4.3.0 requires 8.3 or newer.
Plugin dependencies
plugins maps a slug to a constraint. The manager:
- Verifies the plugin is installed.
- Verifies it is not in an error state.
- Verifies its version satisfies the constraint.
- Boots it first.
Missing, unmet, or circular dependencies isolate only the dependent plugin.
Provides
"provides": [
"checkout.price.calculate",
"user.registered",
"dashboard.referral_panel",
"admin.menu"
]
provides is documentation and discovery metadata. It does not register anything; your boot()
method does. Keep it accurate so operators can see what a plugin touches before activating it.
Use the exact hook, slot, or registry domain name.
Capabilities
"capabilities": ["outbound-https"]
Capabilities declare what a plugin may do outside its directory. They are advisory today; the marketplace review process uses them to decide whether a plugin needs extra scrutiny.
| Capability | Meaning |
|---|---|
outbound-https | Makes outbound HTTPS requests |
filesystem-write | Writes files outside its own storage |
background-jobs | Dispatches queue jobs |
scheduled-tasks | Registers a scheduler entry |
payment-gateway | Charges or refunds money |
user-data | Stores personal data |
Do not declare a capability you do not use. Do not omit one you do.
Legacy name
"legacyName": "MyPlugin"
legacyName preserves compatibility with a ported module:
- Registers the lowercased legacy view namespace.
- Registers the lowercased legacy translation namespace.
- Merges
config/<legacy>.phpunder the legacy config key. - Lets
plugin_resolve_slug()match the old module name.
Example: hourly-job keeps legacyName: "HourlyJob", so hourlyjob:: views and config('hourlyjob')
continue to resolve.
Do not set legacyName on a new plugin.
Alias
"alias": "myplugin"
alias is optional. Core's legacy metadata reader exposes it for plugins that still use the module
era naming. New plugins do not need it.
xilancerMetaData
This object holds everything that used to live in module.json.
plugin_type
"xilancerMetaData": {
"plugin_type": "external"
}
Use external for marketplace plugins. Core uses this to list third-party plugins separately from
bundled modules.
hooks
Maps a legacy Blade position to view files:
"hooks": {
"head_end": ["assets.head"],
"body_end": ["assets.banner", "announcement.popup"]
}
Supported positions: head_start, head_end, body_start, body_end.
Each entry is a view name inside the plugin's namespace. The renderer strips .blade, .blade.php,
and .php, then renders <slug>::<name>. For announcements, announcement.popup resolves to
announcements::announcement.popup.
Use these only for assets and global overlays. For page-specific HTML, register a slot instead.
admin_settings.menu_item
"admin_settings": {
"menu_item": [
{
"id": "my-plugin-menu",
"label": "My Plugin",
"route": "admin.my-plugin.index",
"parent": null,
"icon": "fa-solid fa-star",
"permissions": "my-plugin-manage",
"class": "bg-success"
},
{
"id": "my-plugin-settings",
"label": "Settings",
"route": "admin.my-plugin.settings",
"parent": "my-plugin-menu",
"icon": "fa-solid fa-gear",
"permissions": "my-plugin-manage",
"class": "bg-success"
}
]
}
| Key | Required | Meaning |
|---|---|---|
id | Yes | Unique menu identifier. Children reference it as parent. |
label | Yes | Sidebar label. |
route | Yes | Named route. Items with undefined routes are skipped. |
parent | No | Parent menu id, or null for a top-level item. |
icon | No | Font Awesome class. |
permissions | No | Permission slug checked before rendering. |
class | No | Extra CSS class for the item. |
Menus are rendered only for active plugins. A user without the permission does not see the item.
pageBuilderAddon
"pageBuilderAddon": [
{ "addon": "MyPlugin\\PageBuilder\\HeroBlock" }
]
Registers page-builder addon classes. Only ported modules need this. New plugins should use slots.
Validation rules
The validator (App\Services\PluginValidator) checks:
- Directory exists.
plugin.jsonexists and is valid JSON.- Required fields are present and valid.
- Entry file exists.
- Every PHP file passes
php -l. - PHP version satisfies
requires.php. - Framework version satisfies
requires.framework. - Plugin dependencies are active.
- Entry class exists, implements
PluginInterface, and its constructor acceptsPluginContext. - A sandboxed boot test succeeds.
Warnings do not block activation. Errors do. Common warnings:
php -lis unavailable.- Composer's autoloader is unavailable, so the boot test is skipped.
- A manifest declares a capability it does not appear to use.
Manifest anti-patterns
| Anti-pattern | Problem |
|---|---|
version: "1.0" with a v prefix | Fails the numeric dotted rule |
| Uppercase slug | Fails the slug rule |
entry.class without the namespace | Class not found |
autoload mapping to the plugin root | Namespace collisions |
Declaring every hook in provides | Misleading for operators |
Shipping module.json | Ignored; metadata must be in plugin.json |
requires.plugins pointing to itself | Circular dependency error |
Menu route that does not exist | Item silently skipped |
Last updated: October 21, 2026

