From bd8b4fa6c1738dcfa4bd4fd02117734f2e17ceb0 Mon Sep 17 00:00:00 2001 From: Ollama Date: Fri, 6 Mar 2026 12:55:44 +0000 Subject: [PATCH] feat(plugins): Support self-contained plugin directories - PluginManager now recursively scans app/Plugins/ to discover plugins - Supports both single-file plugins (MyPlugin.php) and directory plugins (MyPlugin/MyPlugin.php) - Plugins can contain their own Models, Controllers, Views, Libraries, Helpers - Uses PSR-4 namespacing: App\Plugins\PluginName for files, App\Plugins\PluginName\Subdir for subdirectories - Users can install plugins by simply dropping a folder into app/Plugins/ - Updated README with comprehensive documentation on both plugin formats This makes plugin installation much easier - just drop the plugin folder and it works. --- app/Libraries/Plugins/PluginManager.php | 37 +- app/Plugins/README.md | 579 ++++++++++++------------ 2 files changed, 331 insertions(+), 285 deletions(-) diff --git a/app/Libraries/Plugins/PluginManager.php b/app/Libraries/Plugins/PluginManager.php index 467d367a7..184674173 100644 --- a/app/Libraries/Plugins/PluginManager.php +++ b/app/Libraries/Plugins/PluginManager.php @@ -4,13 +4,20 @@ namespace App\Libraries\Plugins; use App\Models\Plugin_config; use CodeIgniter\Events\Events; -use DirectoryIterator; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; /** * Plugin Manager * * Discovers, loads, and manages plugins in the OSPOS system. * Plugins are discovered from app/Plugins directory and must implement PluginInterface. + * + * Plugins can be organized in two ways: + * 1. Single file: app/Plugins/MyPlugin.php with namespace App\Plugins + * 2. Plugin directory: app/Plugins/MyPlugin/MyPlugin.php with namespace App\Plugins\MyPlugin + * + * The directory structure allows plugins to contain their own Models, Controllers, Views, etc. */ class PluginManager { @@ -28,7 +35,8 @@ class PluginManager /** * Discover and load all available plugins. * - * Scans the Plugins directory for classes implementing PluginInterface. + * Scans the Plugins directory recursively for classes implementing PluginInterface. + * Supports both single-file plugins and plugin directories. */ public function discoverPlugins(): void { @@ -37,15 +45,19 @@ class PluginManager return; } - $iterator = new DirectoryIterator($this->pluginsPath); + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($this->pluginsPath, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ); + foreach ($iterator as $file) { if ($file->isDir() || $file->getExtension() !== 'php') { continue; } - $className = 'App\\Plugins\\' . $file->getBasename('.php'); + $className = $this->getClassNameFromFile($file->getPathname()); - if (!class_exists($className)) { + if (!$className || !class_exists($className)) { continue; } @@ -61,6 +73,21 @@ class PluginManager } } + /** + * Get the fully-qualified class name from a file path. + * + * @param string $pathname The full path to the PHP file + * @return string|null The class name or null if unable to determine + */ + private function getClassNameFromFile(string $pathname): ?string + { + $relativePath = str_replace($this->pluginsPath . DIRECTORY_SEPARATOR, '', $pathname); + $relativePath = str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath); + $className = 'App\\Plugins\\' . str_replace('.php', '', $relativePath); + + return $className; + } + /** * Register event listeners for all enabled plugins. * diff --git a/app/Plugins/README.md b/app/Plugins/README.md index 4d574bc32..0c4137154 100644 --- a/app/Plugins/README.md +++ b/app/Plugins/README.md @@ -4,6 +4,46 @@ The OSPOS Plugin System allows third-party integrations to extend the application's functionality without modifying core code. Plugins can listen to events, add configuration settings, and integrate with external services. +## Installation + +### Self-Contained Plugin Packages + +Plugins are self-contained packages that can be installed by simply dropping the plugin folder into `app/Plugins/`: + +``` +app/Plugins/ +├── CasposPlugin/ # Plugin directory (self-contained) +│ ├── CasposPlugin.php # Main plugin class (required - must match directory name) +│ ├── Models/ # Plugin-specific models +│ │ └── Caspos_data.php +│ ├── Controllers/ # Plugin-specific controllers +│ │ └── Dashboard.php +│ ├── Views/ # Plugin-specific views +│ │ └── config.php +│ ├── Libraries/ # Plugin-specific libraries +│ ├── Helpers/ # Plugin-specific helpers +│ └── config/ # Configuration files +│ +├── MailchimpPlugin.php # Or single-file plugins (simple plugins) +└── ExamplePlugin.php +``` + +### Installation Steps + +1. **Download the plugin** - Copy the plugin folder/file to `app/Plugins/` +2. **Auto-discovery** - The plugin will be automatically discovered on next page load +3. **Enable** - Enable it from the admin interface (Plugins menu) +4. **Configure** - Configure plugin settings if needed + +### Plugin Discovery + +The PluginManager recursively scans `app/Plugins/` directory: + +- **Single-file plugins**: `app/Plugins/MyPlugin.php` with namespace `App\Plugins\MyPlugin` +- **Directory plugins**: `app/Plugins/MyPlugin/MyPlugin.php` with namespace `App\Plugins\MyPlugin\MyPlugin` + +Both formats are supported, but directory plugins allow for self-contained packages with their own MVC components. + ## Architecture ### Plugin Interface @@ -43,7 +83,7 @@ class MyPlugin extends BasePlugin ### Plugin Manager The `PluginManager` class handles: -- Plugin discovery from `app/Plugins/` directory +- Plugin discovery from `app/Plugins/` directory (recursive scan) - Loading and registering enabled plugins - Managing plugin settings @@ -62,7 +102,9 @@ OSPOS fires these events that plugins can listen to: ## Creating a Plugin -### 1. Create the Plugin Class +### Simple Plugin (Single File) + +For plugins that only need to listen to events without complex UI or database tables: ```php log('info', "Processing sale: {$saleData['sale_id_num']}"); } @@ -118,12 +158,11 @@ class MyPlugin extends BasePlugin return; } - // Your logic here + $this->log('info', "Item changed: {$itemId}"); } public function install(): bool { - // Set default settings $this->setSetting('api_key', ''); $this->setSetting('enabled', '0'); return true; @@ -136,15 +175,192 @@ class MyPlugin extends BasePlugin } ``` -### 2. Create Configuration View (Optional) +### Complex Plugin (Self-Contained Directory) + +For plugins that need database tables, controllers, models, and views: + +``` +app/Plugins/ +└── CasposPlugin/ # Plugin directory + ├── CasposPlugin.php # Main class - namespace: App\Plugins\CasposPlugin + ├── Models/ # Plugin models - namespace: App\Plugins\CasposPlugin\Models + │ └── Caspos_data.php + ├── Controllers/ # Plugin controllers - namespace: App\Plugins\CasposPlugin\Controllers + │ └── Dashboard.php + ├── Views/ # Plugin views + │ ├── config.php + │ └── dashboard.php + └── Libraries/ # Plugin libraries - namespace: App\Plugins\CasposPlugin\Libraries + └── Api_client.php +``` + +**Main Plugin Class:** ```php dataModel === null) { + $this->dataModel = new Caspos_data(); + } + return $this->dataModel; + } + + public function onItemSale(array $saleData): void + { + if (!$this->isEnabled()) { + return; + } + + // Use internal model + $this->getDataModel()->saveSaleRecord($saleData); + + // Use internal library + $apiClient = new \App\Plugins\CasposPlugin\Libraries\Api_client(); + $apiClient->sendToGovernment($saleData); + } + + public function install(): bool + { + // Create plugin-specific database table + $this->getDataModel()->createTable(); + + // Set default settings + $this->setSetting('api_url', ''); + $this->setSetting('api_key', ''); + return true; + } + + public function uninstall(): bool + { + // Drop plugin table + $this->getDataModel()->dropTable(); + return true; + } + + public function getConfigView(): ?string + { + return 'Plugins/CasposPlugin/Views/config'; + } +} +``` + +**Plugin Model:** + +```php + ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true], + 'sale_id' => ['type' => 'INT', 'constraint' => 11], + 'fiscal_number' => ['type' => 'VARCHAR', 'constraint' => 100], + 'api_response' => ['type' => 'TEXT'], + 'created_at' => ['type' => 'TIMESTAMP'] + ]; + + $forge->addField($fields); + $forge->addKey('id', true); + $forge->createTable($this->table, true); + } + + public function dropTable(): void + { + $forge = \Config\Database::forge(); + $forge->dropTable($this->table, true); + } +} +``` + +**Plugin Controller:** + +```php +dataModel = new Caspos_data(); + } + + public function getIndex(): void + { + $data = ['records' => $this->dataModel->orderBy('created_at', 'DESC')->findAll(50)]; + echo view('Plugins/CasposPlugin/Views/dashboard', $data); + } +} +``` + +**Plugin View:** + +```php +getSettings(); $this->saveSettings(['key1' => 'value1', 'key2' => 'value2']); ``` -Settings are prefixed with the plugin ID (e.g., `my_plugin_api_key`). +Settings are prefixed with the plugin ID (e.g., `caspos_api_key`) and stored in `ospos_plugin_config` table. -## Example Plugins +## Namespace Reference -### Example Plugin (app/Plugins/ExamplePlugin.php) -A demonstration plugin that logs events to the debug log. - -### Mailchimp Plugin (app/Plugins/MailchimpPlugin.php) -Integrates with Mailchimp to sync customer data. +| File Location | Namespace | +|--------------|-----------| +| `app/Plugins/MyPlugin.php` | `App\Plugins\MyPlugin` | +| `app/Plugins/CasposPlugin/CasposPlugin.php` | `App\Plugins\CasposPlugin\CasposPlugin` | +| `app/Plugins/CasposPlugin/Models/Caspos_data.php` | `App\Plugins\CasposPlugin\Models\Caspos_data` | +| `app/Plugins/CasposPlugin/Controllers/Dashboard.php` | `App\Plugins\CasposPlugin\Controllers\Dashboard` | +| `app/Plugins/CasposPlugin/Libraries/Api_client.php` | `App\Plugins\CasposPlugin\Libraries\Api_client` | ## Database @@ -190,272 +408,15 @@ CREATE TABLE ospos_plugin_config ( ); ``` +For custom tables, plugins can create them during `install()` and drop them during `uninstall()`. + ## Event Flow 1. Application triggers event: `Events::trigger('item_sale', $data)` -2. PluginManager loads enabled plugins -3. Each plugin registers its listeners via `registerEvents()` +2. PluginManager recursively scans `app/Plugins/` directory +3. Each enabled plugin registers its listeners via `registerEvents()` 4. Events::on() callbacks are invoked automatically -## Plugin Directory Structure - -### Core Plugin System Files - -``` -app/ -├── Libraries/ -│ └── Plugins/ -│ ├── PluginInterface.php # Contract all plugins must implement -│ ├── BasePlugin.php # Abstract base class with common functionality -│ └── PluginManager.php # Discovers and loads plugins -├── Models/ -│ └── Plugin_config.php # Model for plugin settings storage -├── Controllers/ -│ └── Plugins/ -│ └── Manage.php # Admin controller for plugin management -└── Views/ - └── plugins/ - └── manage.php # Plugin management admin view -``` - -### Plugin Files Organization - -Plugins are organized with a clear separation of concerns: - -``` -app/ -├── Plugins/ # EVENT ORCHESTRATORS -│ ├── ExamplePlugin.php # Simple plugin (event handlers only) -│ ├── MailchimpPlugin.php # Integration plugin -│ └── CasposPlugin.php # Complex plugin needing MVC -│ -├── Models/Plugins/ # DATA MODELS (for plugins needing custom tables) -│ └── Caspos_data.php # Model for CASPOS API data -│ -├── Controllers/Plugins/ # ADMIN CONTROLLERS (for plugin config UI) -│ └── Caspos.php # Controller for CASPOS admin interface -│ -└── Views/Plugins/ # ADMIN VIEWS (for plugin configuration) - ├── example/ - │ └── config.php - ├── mailchimp/ - │ └── config.php - └── caspos/ - └── config.php -``` - -## Plugin Architecture Patterns - -### Simple Plugin (Event Handlers Only) - -For plugins that only need to listen to events and don't require custom database tables or complex admin UI: - -```php -// app/Plugins/ExamplePlugin.php -class ExamplePlugin extends BasePlugin -{ - public function registerEvents(): void - { - Events::on('item_sale', [$this, 'onItemSale']); - } - - public function onItemSale(array $saleData): void - { - // Simple logic - just log or make API calls - $this->log('info', "Sale processed: {$saleData['sale_id_num']}"); - } -} -``` - -### Complex Plugin (Full MVC) - -For plugins that need database tables, complex admin UI, or business logic: - -**1. Plugin Class (Event Orchestrator)** - Entry point that registers events and coordinates with MVC components: - -```php -// app/Plugins/CasposPlugin.php -namespace App\Plugins; - -use App\Libraries\Plugins\BasePlugin; -use App\Models\Plugins\Caspos_data; -use CodeIgniter\Events\Events; - -class CasposPlugin extends BasePlugin -{ - private ?Caspos_data $dataModel = null; - - public function registerEvents(): void - { - Events::on('item_sale', [$this, 'onItemSale']); - Events::on('item_change', [$this, 'onItemChange']); - } - - private function getDataModel(): Caspos_data - { - if ($this->dataModel === null) { - $this->dataModel = new Caspos_data(); - } - return $this->dataModel; - } - - public function onItemSale(array $saleData): void - { - if (!$this->isEnabled()) { - return; - } - - // Use the model for data persistence - $this->getDataModel()->saveSaleRecord($saleData); - - // Call external API - $this->sendToGovernmentApi($saleData); - } - - private function sendToGovernmentApi(array $saleData): void - { - // Integration logic - } - - public function install(): bool - { - // Create plugin-specific database table - $this->getDataModel()->createTable(); - - // Set default settings - $this->setSetting('api_url', ''); - $this->setSetting('api_key', ''); - return true; - } -} -``` - -**2. Model (Data Persistence)** - For plugins needing custom database tables: - -```php -// app/Models/Plugins/Caspos_data.php -namespace App\Models\Plugins; - -use CodeIgniter\Model; - -class Caspos_data extends Model -{ - protected $table = 'caspos_records'; - protected $primaryKey = 'id'; - protected $allowedFields = [ - 'sale_id', - 'fiscal_number', - 'api_response', - 'created_at' - ]; - - public function createTable(): void - { - $forge = \Config\Database::forge(); - - $fields = [ - 'id' => [ - 'type' => 'INT', - 'constraint' => 11, - 'unsigned' => true, - 'auto_increment' => true - ], - 'sale_id' => [ - 'type' => 'INT', - 'constraint' => 11 - ], - 'fiscal_number' => [ - 'type' => 'VARCHAR', - 'constraint' => 100 - ], - 'api_response' => [ - 'type' => 'TEXT' - ], - 'created_at' => [ - 'type' => 'TIMESTAMP' - ] - ]; - - $forge->addField($fields); - $forge->addKey('id', true); - $forge->addKey('sale_id'); - $forge->createTable($this->table, true); - } - - public function saveSaleRecord(array $saleData): bool - { - return $this->insert([ - 'sale_id' => $saleData['sale_id_num'], - 'fiscal_number' => $saleData['fiscal_number'] ?? '', - 'api_response' => '' - ]); - } -} -``` - -**3. Controller (Admin UI)** - For plugin configuration pages: - -```php -// app/Controllers/Plugins/Caspos.php -namespace App\Controllers\Plugins; - -use App\Controllers\Secure_Controller; -use App\Models\Plugins\Caspos_data; - -class Caspos extends Secure_Controller -{ - private Caspos_data $dataModel; - - public function __construct() - { - parent::__construct('plugins'); - $this->dataModel = new Caspos_data(); - } - - public function getIndex(): void - { - $data = [ - 'records' => $this->dataModel->orderBy('created_at', 'DESC')->findAll(50) - ]; - echo view('Plugins/caspos/dashboard', $data); - } - - public function postTestConnection(): \CodeIgniter\HTTP\ResponseInterface - { - // Test API connection - return $this->response->setJSON(['success' => true]); - } -} -``` - -**4. Views (Admin Interface)** - Configuration and dashboard: - -```php -// app/Views/Plugins/caspos/config.php - -
- - -
-
- - -
- - -``` - -### Architecture Summary - -| Component | Directory | Purpose | -|-----------|-----------|---------| -| Event Orchestrator | `app/Plugins/` | Implements `PluginInterface`, registers listeners, coordinates logic | -| Data Models | `app/Models/Plugins/` | Database models for plugin-specific tables | -| Admin Controllers | `app/Controllers/Plugins/` | Controllers for plugin configuration UI | -| Admin Views | `app/Views/Plugins/` | Views for plugin configuration | - -The **plugin class** in `app/Plugins/` acts as the entry point - it listens to events and coordinates with its own MVC components as needed. This keeps the architecture modular and maintains separation of concerns. - ## Testing Enable plugin logging to debug: @@ -466,4 +427,62 @@ $this->log('info', 'Info message'); $this->log('error', 'Error message'); ``` -Check logs in `writable/logs/`. \ No newline at end of file +Check logs in `writable/logs/`. + +## Example Plugin Structure + +### Simple Event-Listening Plugin + +``` +app/Plugins/ +└── ExamplePlugin.php # Just logging - no models/controllers/views needed +``` + +### Complex Self-Contained Plugin + +``` +app/Plugins/ +└── CasposPlugin/ + ├── CasposPlugin.php # Main class, event handling + ├── Models/ + │ ├── Caspos_data.php # Database model + │ └── Caspos_transaction.php + ├── Controllers/ + │ ├── Dashboard.php # Admin dashboard + │ └── Settings.php # Settings page + ├── Views/ + │ ├── config.php # Configuration form + │ ├── dashboard.php # Dashboard view + │ └── transaction_list.php + ├── Libraries/ + │ └── Api_client.php # Government API client + ├── Helpers/ + │ └── caspos_helper.php # Helper functions + └── config/ + └── routes.php # Custom routes (optional) +``` + +This structure allows users to install a plugin by simply: + +```bash +# Download/extract +cp -r CasposPlugin/ /path/to/ospos/app/Plugins/ + +# Plugin auto-discovered and available in admin UI +``` + +## Distributing Plugins + +Plugin developers can package their plugins as zip files: + +``` +CasposPlugin-1.0.0.zip +└── CasposPlugin/ + ├── CasposPlugin.php + ├── Models/ + ├── Controllers/ + ├── Views/ + └── README.md # Plugin documentation +``` + +Users extract the zip to `app/Plugins/` and the plugin is ready to use. \ No newline at end of file