The MODX CLI Plugin Architecture provides a powerful and flexible system for extending the CLI with custom functionality. Plugins can register commands, listen to hooks (events), and integrate seamlessly with the core application.
Key Features:
- Automatic plugin discovery and loading
- Hook system for event-driven extensions
- Configuration management per plugin
- Enable/disable functionality
- Version requirement checking
- PSR-compliant interfaces
Create a plugin by implementing the PluginInterface or extending AbstractPlugin:
<?php
// plugins/example-plugin/Plugin.php
namespace MyVendor\ExamplePlugin;
use MODX\CLI\Plugin\AbstractPlugin;
use MODX\CLI\Application;
class Plugin extends AbstractPlugin
{
public function getName(): string
{
return 'example-plugin';
}
public function getVersion(): string
{
return '1.0.0';
}
public function getDescription(): string
{
return 'An example plugin demonstrating MODX CLI plugin capabilities';
}
public function getAuthor(): string
{
return 'Your Name';
}
public function getCommands(): array
{
return [
\MyVendor\ExamplePlugin\Command\ExampleCommand::class
];
}
public function getHooks(): array
{
return [
'command.before' => [$this, 'onCommandBefore'],
'command.after' => [$this, 'onCommandAfter']
];
}
public function onCommandBefore(array $context): array
{
// Do something before command execution
$this->getApplication()->getLogger()->info('Command starting: {command}', [
'command' => $context['command'] ?? 'unknown'
]);
return $context;
}
public function onCommandAfter(array $context): array
{
// Do something after command execution
return $context;
}
}plugins/
└── example-plugin/
├── Plugin.php # Main plugin class (required)
├── Command/ # Plugin commands
│ └── ExampleCommand.php
└── composer.json # Optional: dependencies
- Project-local plugins: Place in
.modx-cli/plugins/directory (from your project root) - Global plugins: Place in CLI installation
plugins/directory - Composer packages: Install as dependencies and register the plugin path
Plugins are discovered and loaded automatically when the CLI starts.
- PluginInterface - Defines the contract all plugins must implement
- AbstractPlugin - Base class providing common functionality
- PluginManager - Handles plugin discovery, loading, and lifecycle
- HookManager - Manages event hooks and execution
- Application Integration - Seamless integration with the CLI app
- Discovery - PluginManager scans plugin directories
- Loading - Plugin classes are instantiated and registered
- Validation - Requirements (PHP/CLI version) are checked
- Configuration - Plugin config is loaded and applied
- Initialization - Plugin's
initialize()method is called - Registration - Commands and hooks are registered
- Execution - Plugin is ready and responds to events
interface PluginInterface
{
// Identity
public function getName(): string;
public function getVersion(): string;
public function getDescription(): string;
public function getAuthor(): string;
// Requirements
public function getMinPhpVersion(): string;
public function getMinCliVersion(): string;
// Lifecycle
public function initialize(Application $app): void;
// State
public function isEnabled(): bool;
public function enable(): void;
public function disable(): void;
// Configuration
public function getConfig(): array;
public function setConfig(array $config): void;
// Extensions
public function getCommands(): array;
public function getHooks(): array;
}When extending AbstractPlugin, you get these additional helpers:
protected function getConfigValue(string $key, mixed $default = null): mixed
protected function setConfigValue(string $key, mixed $value): void
protected function getApplication(): ?ApplicationThe CLI fires hooks at key points in the application lifecycle:
| Hook Name | Context | Description |
|---|---|---|
app.bootstrap |
['app' => Application] |
Application bootstrapped |
command.before |
['command' => string, 'input' => InputInterface] |
Before command execution |
command.after |
['command' => string, 'result' => int] |
After command execution |
command.error |
['command' => string, 'error' => Throwable] |
Command threw an exception |
Return hook registrations from your plugin's getHooks() method:
public function getHooks(): array
{
return [
'command.before' => [$this, 'onCommandBefore'],
'command.after' => function($context) {
// Anonymous function handler
return $context;
}
];
}Hook handlers receive a context array and can modify it:
public function onCommandBefore(array $context): array
{
// Read from context
$commandName = $context['command'];
// Modify context (changes passed to next handler)
$context['start_time'] = microtime(true);
return $context;
}Register hooks with priority (higher = executed first):
$app->getHookManager()->register('command.before', $handler, 20); // High priority
$app->getHookManager()->register('command.before', $handler2, 10); // Default priority
$app->getHookManager()->register('command.before', $handler3, 5); // Low priorityDefine default configuration in your plugin:
class Plugin extends AbstractPlugin
{
protected array $config = [
'enabled' => true,
'option1' => 'default_value',
'option2' => 100
];
public function getConfig(): array
{
return $this->config;
}
}Users can configure plugins via .modx-cli/plugins.yaml (project-local):
example-plugin:
enabled: true
option1: custom_value
option2: 200// In your plugin
$value = $this->getConfigValue('option1', 'fallback');
$this->setConfigValue('option2', 300);Return command class names from getCommands():
public function getCommands(): array
{
return [
\MyVendor\ExamplePlugin\Command\ExampleCommand::class,
\MyVendor\ExamplePlugin\Command\AnotherCommand::class
];
}<?php
namespace MyVendor\ExamplePlugin\Command;
use MODX\CLI\Command\BaseCmd;
class ExampleCommand extends BaseCmd
{
protected $name = 'plugin:example';
protected $description = 'Example plugin command';
protected function process()
{
$this->info('Hello from the example plugin!');
// Access logger
$this->logInfo('Command executed');
// Access hook manager
$hooks = $this->getApplication()->getHookManager();
return 0;
}
}Track all command executions:
class LoggingPlugin extends AbstractPlugin
{
public function getName(): string
{
return 'command-logger';
}
public function getVersion(): string
{
return '1.0.0';
}
public function getDescription(): string
{
return 'Logs all command executions to a file';
}
public function getHooks(): array
{
return [
'command.before' => [$this, 'logCommandStart'],
'command.after' => [$this, 'logCommandEnd'],
'command.error' => [$this, 'logCommandError']
];
}
public function logCommandStart(array $context): array
{
$logFile = $this->getConfigValue('log_file', 'command.log');
file_put_contents($logFile, sprintf(
"[%s] START: %s\n",
date('Y-m-d H:i:s'),
$context['command']
), FILE_APPEND);
$context['start_time'] = microtime(true);
return $context;
}
public function logCommandEnd(array $context): array
{
$duration = microtime(true) - ($context['start_time'] ?? 0);
$logFile = $this->getConfigValue('log_file', 'command.log');
file_put_contents($logFile, sprintf(
"[%s] END: %s (%.2fs, exit: %d)\n",
date('Y-m-d H:i:s'),
$context['command'],
$duration,
$context['result'] ?? 0
), FILE_APPEND);
return $context;
}
public function logCommandError(array $context): array
{
$logFile = $this->getConfigValue('log_file', 'command.log');
file_put_contents($logFile, sprintf(
"[%s] ERROR: %s - %s\n",
date('Y-m-d H:i:s'),
$context['command'],
$context['error']->getMessage()
), FILE_APPEND);
return $context;
}
}Send notifications when commands complete:
class NotificationPlugin extends AbstractPlugin
{
public function getName(): string
{
return 'notifications';
}
public function getVersion(): string
{
return '1.0.0';
}
public function getDescription(): string
{
return 'Sends notifications on command completion';
}
public function getHooks(): array
{
return [
'command.after' => [$this, 'sendNotification']
];
}
public function sendNotification(array $context): array
{
$command = $context['command'];
$result = $context['result'] ?? 0;
// Only notify for long-running commands
$duration = microtime(true) - ($context['start_time'] ?? microtime(true));
if ($duration < 10) {
return $context;
}
$message = $result === 0
? "✅ Command completed: {$command}"
: "❌ Command failed: {$command}";
// Send notification (example using a webhook)
$webhook = $this->getConfigValue('webhook_url');
if ($webhook) {
file_get_contents($webhook . '?' . http_build_query([
'message' => $message,
'duration' => round($duration, 2)
]));
}
return $context;
}
}Add project-specific commands:
class ProjectCommandsPlugin extends AbstractPlugin
{
public function getName(): string
{
return 'project-commands';
}
public function getVersion(): string
{
return '1.0.0';
}
public function getDescription(): string
{
return 'Project-specific custom commands';
}
public function getCommands(): array
{
return [
\MyProject\Command\DeployCommand::class,
\MyProject\Command\BackupCommand::class,
\MyProject\Command\SyncCommand::class
];
}
}$pluginManager = $app->getPluginManager();
// Get all plugins
$allPlugins = $pluginManager->getPlugins();
// Get enabled plugins only
$enabledPlugins = $pluginManager->getEnabledPlugins();
// Get specific plugin
$plugin = $pluginManager->getPlugin('example-plugin');// Enable a plugin
$pluginManager->enablePlugin('example-plugin');
// Disable a plugin
$pluginManager->disablePlugin('example-plugin');
// Check if enabled
if ($plugin->isEnabled()) {
// Plugin is active
}$plugin = $pluginManager->getPlugin('example-plugin');
echo $plugin->getName(); // "example-plugin"
echo $plugin->getVersion(); // "1.0.0"
echo $plugin->getDescription(); // "An example plugin..."
echo $plugin->getAuthor(); // "Your Name"// ✅ Good
public function getName(): string
{
return 'backup-automation';
}
// ❌ Avoid
public function getName(): string
{
return 'plugin1';
}public function getMinPhpVersion(): string
{
return '8.0'; // Require PHP 8.0+
}
public function getMinCliVersion(): string
{
return '1.0.0'; // Require CLI 1.0.0+
}public function getVersion(): string
{
return '1.2.3'; // MAJOR.MINOR.PATCH
}public function onCommandError(array $context): array
{
try {
// Your error handling
$this->notifyAdmin($context['error']);
} catch (\Throwable $e) {
// Don't break the command - just log
$this->getApplication()->getLogger()->warning(
'Plugin error handler failed: {error}',
['error' => $e->getMessage()]
);
}
return $context;
}// Allow users to configure your plugin
protected array $config = [
'enabled' => true,
'api_key' => '',
'timeout' => 30
];
public function initialize(Application $app): void
{
parent::initialize($app);
if (!$this->getConfigValue('api_key')) {
$app->getLogger()->warning(
'Plugin {name} is missing api_key configuration',
['name' => $this->getName()]
);
}
}Create a README.md in your plugin directory:
# Example Plugin
## Installation
Place in `.modx-cli/plugins/example-plugin/` (project root)
## Configuration
Edit `.modx-cli/plugins.yaml` (project root):
\`\`\`yaml
example-plugin:
enabled: true
option1: value
\`\`\`
## Usage
\`\`\`bash
modx plugin:example
\`\`\`Problem: Plugin doesn't appear in the CLI
Solutions:
- Ensure
Plugin.phpexists in the plugin directory - Check plugin namespace and class name
- Verify the class implements
PluginInterface - Check logs for loading errors
Problem: Plugin disabled due to version requirements
Solutions:
- Update PHP version:
php -v - Update MODX CLI:
composer global update finetuned/modx-cli - Lower plugin requirements if appropriate
Problem: Hook handlers not being called
Solutions:
- Verify hook name spelling
- Check plugin is enabled
- Ensure
initialize()was called - Use
getHookManager()->getHookNames()to see registered hooks
Problem: Plugin command not available
Solutions:
- Verify command class exists
- Check namespace and class name
- Ensure command extends
BaseCmd - Clear cache:
modx cache:clear(if implemented)
// Plugin discovery and loading
public function loadPlugins(): void
public function addPluginDirectory(string $directory): void
// Plugin access
public function getPlugin(string $name): ?PluginInterface
public function getPlugins(): array
public function getEnabledPlugins(): array
// Plugin management
public function registerPlugin(PluginInterface $plugin): void
public function enablePlugin(string $name): bool
public function disablePlugin(string $name): bool
// Configuration
public function setLogger(LoggerInterface $logger): void// Hook registration
public function register(string $hookName, callable $handler, int $priority = 10): void
public function unregister(string $hookName, ?callable $handler = null): void
// Hook execution
public function execute(string $hookName, array $context = []): array
// Hook inspection
public function hasHook(string $hookName): bool
public function getHookNames(): array
public function getHandlerCount(string $hookName): int
// Statistics
public function getStats(): array
// Management
public function clear(): void
public function setLogger(LoggerInterface $logger): voidThe Plugin Architecture provides:
- ✅ Extensible: Easy to add custom functionality
- ✅ Flexible: Hooks at key application points
- ✅ Configurable: Per-plugin configuration support
- ✅ Manageable: Enable/disable plugins easily
- ✅ Safe: Version checking and error handling
- ✅ Standard: PSR-compliant interfaces
Build powerful extensions for MODX CLI without modifying core code!