Atomic plugins extend Engine\Atomic\App\Plugin and are coordinated by Engine\Atomic\App\PluginManager.
Core plugin bootstrap happens in two stages:
App::register_core_plugins(...)- If no classes are passed explicitly, it reads
config/providers.phpand uses thepluginskey. - Each class is instantiated as
new $pluginClass($this)and registered in the manager.
- If no classes are passed explicitly, it reads
App::register_plugins()PluginManager::load_user_plugins()PluginManager::register_all()PluginManager::boot_all()
User plugins are loaded from the configured USER_PLUGINS directory, which defaults to plugins/ at the project root. The manager scans each direct subdirectory for plugin.php and only loads files that resolve inside the real USER_PLUGINS path.
User plugins may ship their own Composer dependencies. If <plugin>/vendor/autoload.php exists, the plugin manager loads it before <plugin>/plugin.php. If <plugin>/composer.json exists but vendor/autoload.php is missing, startup logs a warning. Atomic does not run composer install for plugins at runtime.
In the current plugin system, plugin-local Composer autoloaders are still registered in the PHP process globally. This means dependency class names are not isolated per plugin. If two plugins, or a plugin and the application, load incompatible versions of the same package namespace, PHP may use whichever class is loaded first and the versions can conflict.
For production applications, prefer installing shared dependencies in the root application Composer setup. Use plugin-local Composer dependencies for simple or self-contained plugins where package conflicts are unlikely. Plugins that need hard dependency isolation should ship scoped/prefixed dependencies as part of their own build process.
All plugins extend the abstract base class:
abstract class Plugin
{
protected App $atomic;
protected string $name;
protected string $version = '1.0.0';
protected string $path;
protected bool $enabled = true;
protected array $dependencies = [];
public function __construct(?App $atomic = null);
abstract protected function get_name(): string;
protected function get_path(): string;
public function register(): void {}
public function boot(): void {}
public function activate(): void {}
public function deactivate(): void {}
public function is_enabled(): bool;
public function set_enabled(bool $enabled): void;
public function get_version(): string;
public function get_dependencies(): array;
public function get_plugin_name(): string;
public function get_plugin_path(): string;
public function get_migrations_path(): ?string;
}Notes:
get_name()is the canonical plugin identifier used by the manager.get_path()is derived from the plugin class file via reflection.get_migrations_path()returns<plugin_path>/Migrationsonly when that directory exists; otherwise it returnsnull.
register(): called byPluginManager::register_all()for enabled plugins.boot(): called byPluginManager::boot_all()for plugins that registered successfully.activate(): called byenable_plugin(...)/PluginManager::enable(...).deactivate(): called bydisable_plugin(...)/PluginManager::disable(...).
Important runtime rules:
- Duplicate plugin names are ignored. The first registered plugin wins.
- Disabled plugins are kept in the manager but skipped by
register_all(). - Dependency failures do not stop the bootstrap. They are caught and logged, and registration continues for other plugins.
- Boot failures are also caught and logged per plugin.
enable_plugin(...)only sets the plugin as enabled and callsactivate(). It does not automatically runregister()orboot().disable_plugin(...)callsdeactivate(), marks the plugin disabled, and removes it from the internalregisteredandbootedsets.
Declare dependencies by plugin class name:
use Engine\Atomic\Plugins\Google;
protected array $dependencies = [Google::class];Dependency behavior:
- The dependency class must exist and extend
Engine\Atomic\App\Plugin. - The dependency plugin instance must already exist in the manager.
- The dependency plugin must be enabled.
register_all()runs dependencies before dependents, regardless of discovery order.- A dependent plugin is not registered if one of its dependencies fails registration.
boot_all()runs registered dependencies before registered dependents, regardless of discovery order.- A dependent plugin is not booted if one of its dependencies fails booting.
- Dependency cycles are detected, logged, and skipped so unrelated plugins can continue bootstrapping.
This guarantees that if plugin B depends on plugin A, A::register() completes successfully before B::register() runs, and A::boot() completes successfully before B::boot() runs. Keep register() focused on declaring your own services/config, and use boot() for cross-plugin integration that needs dependencies to be registered.
The following helpers are defined in engine/Atomic/Support/helpers.php:
plugin_manager(): PluginManager
get_plugin(string $name): mixed
has_plugin(string $name): bool
enable_plugin(string $name): bool
disable_plugin(string $name): boolExample:
$plugin = get_plugin('Monopay');
if ($plugin !== null) {
// interact with the plugin instance
}After boot_all() finishes its boot loop, Atomic fires ApplicationHook::PLUGINS_LOADED. The later route-loading phase resolves route files for every active route type and loads route files from each booted plugin's routes/ directory. This lets optional modules inspect enabled plugins and register additional route-loader types before route files are resolved.
The filenames come from RouteLoader and depend on the detected request type:
web:web.php,web.error.phpapi:api.phpcli:cli.phptelemetry:telemetry.php
Optional modules may register additional route types:
$atomic->register_route_type('websocket', 'websocket.php');Registering a route type also queues that type for the current bootstrap. Atomic loads matching framework/app route files and plugin route files once during the route-loading pass after plugins boot. For example, the bundled WebSockets plugin registers websocket, then the framework loads routes/websocket.php from the app and from booted plugins before the WebSocket server starts.
Only existing files are required. Exceptions while loading a route file are logged and do not abort the rest of plugin route loading.
Plugin registration and boot run before shared connections are opened by App::open_connections(). If a plugin must use a connection during register() or boot(), it can explicitly call \Engine\Atomic\Core\ConnectionManager::instance()->get_db(), get_redis(), or get_memcached() and handle failure at that point.
Default migration discovery is:
<plugin_path>/Migrations
If your plugin stores migrations elsewhere, override get_migrations_path().
To publish migrations from a registered plugin:
php atomic migrations/publish <plugin-name>Behavior of publish_from_plugin(...):
- It first looks up the plugin by exact name.
- If that fails, it retries with case-insensitive matching.
- If the plugin has no migrations directory, it prints a warning.
- Existing migrations with the same logical name are skipped during publish.
MyPlugin/
composer.json
plugin.php
MyPlugin.php
vendor/
autoload.php
routes/
web.php
api.php
cli.php
websocket.php # if a WebSocket/module route type is registered
Migrations/
create_myplugin_tables.php
plugin.php is the file discovered by load_user_plugins(). A minimal entrypoint looks like:
<?php
declare(strict_types=1);
use Engine\Atomic\App\PluginManager;
if (!defined('ATOMIC_START')) exit;
require_once __DIR__ . '/MyPlugin.php';
PluginManager::instance()->register(new \App\Plugins\MyPlugin\MyPlugin());You can scaffold this layout with:
php atomic plugin/make MyPlugin<?php
declare(strict_types=1);
namespace App\Plugins\MyPlugin;
use Engine\Atomic\App\Plugin;
if (!defined('ATOMIC_START')) exit;
final class MyPlugin extends Plugin
{
protected string $version = '1.0.0';
protected array $dependencies = [];
protected function get_name(): string
{
return 'MyPlugin';
}
public function register(): void
{
$this->atomic->set('PLUGIN.MyPlugin.registered', true);
}
public function boot(): void
{
$this->atomic->set('PLUGIN.MyPlugin.booted', true);
}
public function activate(): void
{
$this->atomic->set('PLUGIN.MyPlugin.active', true);
}
public function deactivate(): void
{
$this->atomic->set('PLUGIN.MyPlugin.active', false);
}
}- Keep
get_name()stable. It is the lookup key used by helpers. - Use plugin class names in
$dependencies, for exampleGoogle::class. - Keep
register()lightweight when it depends on other plugins; defer cross-plugin work toboot(). - Add only the route files needed for the request types you support.
- Add a
Migrations/directory only if the plugin actually ships migrations. - Register core plugins from
providers.php, and user plugins fromplugin.php.