Overview
The Xilancer plugin system has three layers. Each layer has one job, and the boundaries are enforced by convention, validation, and guarded helpers.
Architecture
core/ Laravel application. Calls plugin_*() helpers at seam points.
Never imports plugin classes directly.
plugins/framework/ Zero-dependency engine: hooks, slots, registries, discovery,
boot ordering, manifest validation, version checks.
plugins/<slug>/ Individual plugins. Depend only on PluginsFramework/*.
Core
Core owns the database schema, HTTP routes, authentication, payments, orders, and the admin panel. At specific points it offers:
- A filter so a plugin can transform a value.
- An action so a plugin can react to a committed change.
- A slot so a plugin can render HTML.
- A registry domain so a plugin can contribute structured data.
Core never instantiates a plugin class. It calls plugin_apply_filters(), plugin_do_action(),
plugin_render_slot(), or plugin_registry(). Each helper checks whether the framework is booted
and no-ops safely if it is not.
Framework
The framework is pure PHP with no Laravel dependency. It provides:
| Class | Responsibility |
|---|---|
PluginFramework | Static entry point and version constant |
HookManager | Actions and filters with priority ordering |
SlotRenderer | Named render points with per-renderer error isolation |
Registry | Priority-ordered domains for structured contributions |
PluginManager | Discovery, dependency ordering, activation, boot |
Plugin | Validated manifest value object |
PluginContext | The services handed to a plugin at boot |
Version | Minimal semver constraint checks |
Plugin
A plugin is a directory with:
plugin.json- the manifest.- An entry file and class - the boot code.
- Optional routes, views, migrations, language files, config, and tests.
The entry class implements PluginInterface and receives a PluginContext:
namespace MyPlugin;
use PluginsFramework\Plugins\PluginContext;
use PluginsFramework\Plugins\PluginInterface;
final class MyPlugin implements PluginInterface
{
public function __construct(private readonly PluginContext $ctx)
{
}
public function boot(): void
{
$this->ctx->hooks->addFilter(
'checkout.price.calculate',
fn (float $price, array $context = []): float => $price
);
}
}
Lifecycle at a glance
- Discovery scans
plugins/*/plugin.json, skippingframework. - Ordering sorts by
requires.pluginsusing a depth-first topological sort. - Autoload registers each active plugin's PSR-4 namespaces on the Composer loader.
- Boot instantiates the entry class with
PluginContextand callsboot(). - Wiring loads routes, views, migrations, language files, and config for booted plugins.
- Runtime core calls seams; the framework dispatches to registered callbacks.
- Deactivation removes the slug from
statuses.json; the next request boots without it.
Read Lifecycle & Discovery for the full sequence and failure rules.
Invariants
These are not suggestions. Code review rejects changes that break them.
1. Exception isolation
- A throwing
boot()isolates that plugin and records the error. Other plugins and core continue. - A throwing action bubbles to the caller. Core wraps actions in
plugin_do_action(), which catches and reports, so a faulty action does not break the request. - A throwing slot renderer is skipped per renderer and reported in
SlotRenderer::lastErrors(). - A throwing filter is caught by
plugin_apply_filters(), which returns the input unchanged.
2. No core imports
Plugins import only PluginsFramework\* and PHP builtins. New plugins must not import App\*,
Modules\*, or another plugin's namespace.
Ported premium plugins are a documented exception. They keep internal core references for compatibility, but they communicate with core only through the same seams. Do not copy that pattern into a new plugin.
3. Scalar payloads
Actions receive IDs, strings, numbers, booleans, and arrays. They never receive Eloquent models, requests, passwords, OTPs, or tokens. If you need more data, use the ID to query your own storage or ask core for a new seam.
4. Data filter safety
plugin_filter_model_data() runs your filter, then intersects the result with an allow-list and
re-asserts the original values for protected keys. You can enrich a title or description; you cannot
change user_id, status, password, is_hired, or similar fields.
5. Priority ordering
Lower priority runs first. The default is 10. Ties run in registration order (FIFO).
$ctx->hooks->addFilter('checkout.price.calculate', $callback, 5); // early
$ctx->hooks->addFilter('checkout.price.calculate', $callback, 20); // late
Core's payment.complete handler runs at priority 10. A plugin that contributes a new payment type
registers above 10 and passes non-matching types through unchanged.
6. Activation state is external
plugins/statuses.json is a flat JSON file outside core/. It survives core updates and is easy to
back up. Plugins are inactive by default.
7. Dependency isolation
A missing or unmet dependency isolates only the dependent plugin. It does not prevent unrelated plugins from booting.
What a plugin can contribute
| Contribution | Mechanism | Example |
|---|---|---|
| Transform a price or data array | Filter | Coupons, referrals |
| React to a domain event | Action | Webhooks, affiliate conversions |
| Render HTML in a page | Slot | NDA panel, coupon field |
| Add a payment gateway | Registry gateway | Pesapal, SePay |
| Add a menu item | Registry admin.menu or dashboard.menu | Reports, NDA |
| Add a dashboard widget | Registry widget | Custom metrics |
| Add a storage driver | Registry cloud_storage.driver | Cloud Storage |
| Add an auth provider | Registry auth.provider | AI Toolkit LinkedIn mapper |
| Own routes, views, and tables | Plugin-owned MVC | NDA, Reports |
| Run a scheduled job | Register on the app scheduler in boot() | Freelancer Level reconcile |
What a plugin should not do
- Register a route that shadows a core route.
- Write to core tables it does not own.
- Store secrets in the repository.
- Make an outbound HTTP call during
boot(). - Assume a request context.
boot()runs in console, queue, and HTTP. - Depend on another plugin's internal classes.
- Throw on missing optional configuration. Fail closed for security, open for availability, and log.
Versioning
- Framework version:
PluginFramework::VERSION, currently1.0.0. - Plugin version:
versioninplugin.json, numeric dotted. - Constraint:
requires.frameworkuses a minimal semver syntax (^1.0,>=1.0,1.0.0, and space/comma-separated AND groups). - Plugin dependency:
requires.pluginsmaps a slug to a constraint.
Bump the plugin major version when the manifest or a public contract changes. Bump the minor version for new hooks or settings. Bump the patch version for fixes.
Next steps
- Build the smallest plugin: Creating a Plugin.
- Learn every manifest field: Manifest Reference.
- See the full hook list: Hooks, Slots & Registry.
Last updated: October 21, 2026

