Xgenious/ docs

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:

ClassResponsibility
PluginFrameworkStatic entry point and version constant
HookManagerActions and filters with priority ordering
SlotRendererNamed render points with per-renderer error isolation
RegistryPriority-ordered domains for structured contributions
PluginManagerDiscovery, dependency ordering, activation, boot
PluginValidated manifest value object
PluginContextThe services handed to a plugin at boot
VersionMinimal 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

  1. Discovery scans plugins/*/plugin.json, skipping framework.
  2. Ordering sorts by requires.plugins using a depth-first topological sort.
  3. Autoload registers each active plugin's PSR-4 namespaces on the Composer loader.
  4. Boot instantiates the entry class with PluginContext and calls boot().
  5. Wiring loads routes, views, migrations, language files, and config for booted plugins.
  6. Runtime core calls seams; the framework dispatches to registered callbacks.
  7. 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

ContributionMechanismExample
Transform a price or data arrayFilterCoupons, referrals
React to a domain eventActionWebhooks, affiliate conversions
Render HTML in a pageSlotNDA panel, coupon field
Add a payment gatewayRegistry gatewayPesapal, SePay
Add a menu itemRegistry admin.menu or dashboard.menuReports, NDA
Add a dashboard widgetRegistry widgetCustom metrics
Add a storage driverRegistry cloud_storage.driverCloud Storage
Add an auth providerRegistry auth.providerAI Toolkit LinkedIn mapper
Own routes, views, and tablesPlugin-owned MVCNDA, Reports
Run a scheduled jobRegister 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, currently 1.0.0.
  • Plugin version: version in plugin.json, numeric dotted.
  • Constraint: requires.framework uses a minimal semver syntax (^1.0, >=1.0, 1.0.0, and space/comma-separated AND groups).
  • Plugin dependency: requires.plugins maps 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


Last updated: October 21, 2026

Still stuck?
Our support team is ready to help you get set up.
Get support