Anatomy and Manifest
Folder layout
core/plugins/MyPlugin/
├── plugin.json # manifest (required)
├── MyPluginPlugin.php # provider class (required)
├── Http/ # controllers
├── views/ # Blade views
├── Console/ # Artisan commands
├── Models/ # Eloquent models
├── public/ # optional static files served by the plugin-assets route
│ └── css/app.css
└── database/migrations/ # optional; run on enable
Only plugin.json and the provider class are required. Everything else is up to you. Composer already maps "plugins\\": "plugins/", so provider and helper classes autoload without running composer dump-autoload.
The manifest
plugin.json is validated by App\Plugins\PluginManifestValidator. A plugin with an invalid manifest is logged and never registered.
{
"id": "store-import",
"name": "Store Import",
"version": "1.0.0",
"description": "Teach your chatbot your product catalog.",
"help": "Open Store Import in the user dashboard and enter your store URL.",
"provider": "plugins\\StoreImport\\StoreImportPlugin",
"namespace": "plugins\\StoreImport\\",
"requires": { "helpnest": ">=1.0" },
"capabilities": ["user-menu", "routes", "commands"]
}
| Field | Rule |
|---|---|
id | Required. Lowercase letters, numbers and hyphens only (^[a-z0-9\-]+$). Unique across all plugins. Also used in route prefixes and route names. |
name | Required. Shown in the admin Plugins screen. |
version | Required. A semver string. |
provider | Required. Fully qualified class name. The class must implement PluginContract. |
namespace | Required. The PSR-4 prefix of the plugin. |
description, help | Optional. Shown to admins. |
requires.helpnest, requires.php | Optional version constraints. |
depends, permissions, capabilities, checksum | Optional arrays. |
The capabilities array documents what your plugin touches (for example user-menu, routes, commands, hooks). It is informational, so keep it honest for the admin reading it.
The provider class
The provider is the plugin's entry point. It must implement App\Plugins\Contracts\PluginContract:
<?php
namespace plugins\MyPlugin;
use App\Plugins\Contracts\PluginContract;
use App\Plugins\Contracts\PluginContextContract;
class MyPlugin implements PluginContract
{
public function register(PluginContextContract $context): void {} // container bindings, view namespaces
public function boot(PluginContextContract $context): void {} // hooks, menus, routes, assets, schedules
public function activate(PluginContextContract $context): void {} // runs once when the plugin is enabled
public function deactivate(PluginContextContract $context): void {} // runs when disabled
public function uninstall(PluginContextContract $context): void {} // runs when uninstalled; clean up your data here
}
| Method | Use it for |
|---|---|
register | Binding services into the container and registering a Blade view namespace. Keep it side-effect free. |
boot | Everything that wires the plugin into HelpNest: menus, routes, hooks, event listeners, scheduled commands, assets. |
activate | One-time setup that is not a migration. |
deactivate | Remove anything boot would otherwise leave behind. Data is kept. |
uninstall | Remove your plugin's data and settings. |
Plugin code is only booted when its row is active, so you never need to check that yourself.
The context object
Every method receives a PluginContext. It is how a plugin talks to the framework:
| Call | Purpose |
|---|---|
$context->hooks() | Add actions and filters. |
$context->menus() | Add menu items. |
$context->assets() | Register CSS and JS. |
$context->routes() | Register routes. |
$context->settings() | Read and write plugin options. |
$context->permissions() | Names of the permissions your manifest declares. |
$context->logger() | A logger for your plugin. |
$context->commands([...]) | Register Artisan commands. |
$context->path('views') | Absolute path inside your plugin folder. |
$context->url('...') | Public URL for a file in your plugin's public/ directory. |
Database migrations
Put migrations in database/migrations/. They run when the plugin is enabled and are tracked in the plugin_migrations table, so each runs once. Prefix table names with your plugin's name (for example store_import_stores) to avoid clashing with core or other plugins.

