diff --git a/app/Database/Migrations/20250531000000_PluginConfigTableCreate.php b/app/Database/Migrations/20250531000000_PluginConfigTableCreate.php index 578d7da4e..175f4cc5f 100644 --- a/app/Database/Migrations/20250531000000_PluginConfigTableCreate.php +++ b/app/Database/Migrations/20250531000000_PluginConfigTableCreate.php @@ -6,19 +6,13 @@ use CodeIgniter\Database\Migration; class PluginConfigTableCreate extends Migration { - /** - * Perform a migration step. - */ public function up(): void { - error_log('Migrating plugin_config table started'); + log_message('info', 'Migrating plugin_config table started'); execute_script(APPPATH . 'Database/Migrations/sqlscripts/3.4.1_PluginConfigTableCreate.sql'); } - /** - * Revert a migration step. - */ public function down(): void { } diff --git a/app/Database/Migrations/sqlscripts/3.4.1_PluginConfigTableCreate.sql b/app/Database/Migrations/sqlscripts/3.4.1_PluginConfigTableCreate.sql index f7a32c336..ad51f429f 100644 --- a/app/Database/Migrations/sqlscripts/3.4.1_PluginConfigTableCreate.sql +++ b/app/Database/Migrations/sqlscripts/3.4.1_PluginConfigTableCreate.sql @@ -1,8 +1,7 @@ -CREATE TABLE `ospos_plugin_config` ( +CREATE TABLE IF NOT EXISTS `ospos_plugin_config` ( `key` varchar(100) NOT NULL, `value` text NOT NULL, `created_at` timestamp NOT NULL DEFAULT current_timestamp(), - `updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -ALTER TABLE `ospos_plugin_config` ADD PRIMARY KEY (`key`); \ No newline at end of file + `updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + PRIMARY KEY (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/app/Helpers/plugin_helper.php b/app/Helpers/plugin_helper.php index e241d2d79..12fba373c 100644 --- a/app/Helpers/plugin_helper.php +++ b/app/Helpers/plugin_helper.php @@ -2,14 +2,12 @@ use CodeIgniter\Events\Events; -if (!function_exists('plugin_content')) -{ +if (!function_exists('plugin_content')) { function plugin_content(string $section, array $data = []): string { $results = Events::trigger("view:{$section}", $data); - if (is_array($results)) - { + if (is_array($results)) { return implode('', array_filter($results, fn($r) => is_string($r))); } @@ -17,8 +15,7 @@ if (!function_exists('plugin_content')) } } -if (!function_exists('plugin_content_exists')) -{ +if (!function_exists('plugin_content_exists')) { function plugin_content_exists(string $section): bool { $observers = Events::listRegistered("view:{$section}"); diff --git a/app/Libraries/Plugins/BasePlugin.php b/app/Libraries/Plugins/BasePlugin.php index 87baf6978..188f6f425 100644 --- a/app/Libraries/Plugins/BasePlugin.php +++ b/app/Libraries/Plugins/BasePlugin.php @@ -4,12 +4,6 @@ namespace App\Libraries\Plugins; use App\Models\Plugin_config; -/** - * Base Plugin Class - * - * Abstract base class providing common plugin functionality. - * Plugins can extend this class to reduce boilerplate code. - */ abstract class BasePlugin implements PluginInterface { protected Plugin_config $configModel; @@ -19,45 +13,28 @@ abstract class BasePlugin implements PluginInterface $this->configModel = new Plugin_config(); } - /** - * Default install implementation. - * Override in subclass for custom installation logic. - */ public function install(): bool { return true; } - /** - * Default uninstall implementation. - * Override in subclass for custom uninstallation logic. - */ public function uninstall(): bool { return true; } - /** - * Check if the plugin is enabled. - */ public function isEnabled(): bool { $enabled = $this->configModel->get("{$this->getPluginId()}_enabled"); return $enabled === '1' || $enabled === 'true'; } - /** - * Get a plugin setting. - */ protected function getSetting(string $key, mixed $default = null): mixed { $value = $this->configModel->get("{$this->getPluginId()}_{$key}"); return $value ?? $default; } - /** - * Set a plugin setting. - */ protected function setSetting(string $key, mixed $value): bool { $stringValue = is_array($value) || is_object($value) @@ -67,17 +44,11 @@ abstract class BasePlugin implements PluginInterface return $this->configModel->set("{$this->getPluginId()}_{$key}", $stringValue); } - /** - * Get all plugin settings. - */ public function getSettings(): array { return $this->configModel->getPluginSettings($this->getPluginId()); } - /** - * Save plugin settings. - */ public function saveSettings(array $settings): bool { $prefixedSettings = []; @@ -88,9 +59,6 @@ abstract class BasePlugin implements PluginInterface return $this->configModel->batchSave($prefixedSettings); } - /** - * Log a plugin-specific message. - */ protected function log(string $level, string $message): void { log_message($level, "[Plugin:{$this->getPluginName()}] {$message}"); diff --git a/app/Libraries/Plugins/PluginInterface.php b/app/Libraries/Plugins/PluginInterface.php index df6a4485d..8a032c661 100644 --- a/app/Libraries/Plugins/PluginInterface.php +++ b/app/Libraries/Plugins/PluginInterface.php @@ -2,33 +2,14 @@ namespace App\Libraries\Plugins; -/** - * Plugin Interface - * - * All plugins must implement this interface to be discovered and loaded by the PluginManager. - * This ensures a standard contract for plugin lifecycle and event registration. - */ interface PluginInterface { - /** - * Get the unique identifier for this plugin. - * Should be lowercase with underscores, e.g., 'mailchimp_integration' - */ public function getPluginId(): string; - /** - * Get the human-readable name of the plugin. - */ public function getPluginName(): string; - /** - * Get the plugin description. - */ public function getPluginDescription(): string; - /** - * Get the plugin version. - */ public function getVersion(): string; /** @@ -46,29 +27,19 @@ interface PluginInterface /** * Install the plugin. * - * Called when the plugin is first enabled. Use this to: - * - Create database tables - * - Set default configuration values - * - Run any setup required - * - * @return bool True if installation succeeded + * Called when the plugin is first enabled. Use this to create database tables, + * set default configuration values, and run any setup required. */ public function install(): bool; /** * Uninstall the plugin. * - * Called when the plugin is being removed. Use this to: - * - Remove database tables - * - Clean up configuration - * - * @return bool True if uninstallation succeeded + * Called when the plugin is being removed. Use this to remove database tables, + * clean up configuration, etc. */ public function uninstall(): bool; - /** - * Check if the plugin is enabled. - */ public function isEnabled(): bool; /** @@ -79,18 +50,7 @@ interface PluginInterface */ public function getConfigView(): ?string; - /** - * Get plugin-specific settings for the configuration view. - * - * @return array Settings array to pass to the view - */ public function getSettings(): array; - /** - * Save plugin settings from configuration form. - * - * @param array $settings The settings to save - * @return bool True if settings were saved successfully - */ public function saveSettings(array $settings): bool; } \ No newline at end of file diff --git a/app/Plugins/README.md b/app/Plugins/README.md index 8180e2849..204765eb3 100644 --- a/app/Plugins/README.md +++ b/app/Plugins/README.md @@ -492,6 +492,187 @@ echo form_submit('submit', 'Save'); echo form_close(); ``` +## Internationalization (Language Files) + +Plugins can include their own language files, making them completely self-contained. This allows plugins to provide translations without modifying core language files. + +### Plugin Language Directory Structure + +``` +app/Plugins/ +└── CasposPlugin/ + ├── CasposPlugin.php + ├── Language/ + │ ├── en/ + │ │ └── CasposPlugin.php # English translations + │ ├── es-ES/ + │ │ └── CasposPlugin.php # Spanish translations + │ └── de-DE/ + │ └── CasposPlugin.php # German translations + └── Views/ + └── config.php +``` + +### Language File Format + +Each language file returns an array of translation strings: + +```php + 'CASPOS Integration', + 'caspos_plugin_desc' => 'Azerbaijan government cash register integration', + 'caspos_print_receipt' => 'Print Fiscal Receipt', + 'caspos_fiscal_number' => 'Fiscal Number', + 'caspos_api_url' => 'API URL', + 'caspos_api_key' => 'API Key', + 'caspos_configuration' => 'CASPOS Configuration', + 'caspos_sync_success' => 'Sale synchronized successfully', + 'caspos_sync_failed' => 'Failed to synchronize sale', +]; +``` + +### Loading Language Strings in Plugins + +The `BasePlugin` class can provide a helper method to load plugin-specific language strings: + +```php +lang('caspos_plugin_name'); + } + + public function getPluginDescription(): string + { + return $this->lang('caspos_plugin_desc'); + } + + public function onItemSale(array $saleData): void + { + if (!$this->isEnabled()) { + return; + } + + $result = $this->sendToApi($saleData); + + if ($result['success']) { + $this->log('info', $this->lang('caspos_sync_success')); + } else { + $this->log('error', $this->lang('caspos_sync_failed') . ': ' . $result['error']); + } + } + + protected function lang(string $key, array $data = []): string + { + $language = \Config\Services::language(); + $language->addLanguagePath(APPPATH . 'Plugins/CasposPlugin/Language/'); + return $language->getLine($key, $data); + } +} +``` + +### Using Language Strings in Plugin Views + +```php +addLanguagePath(APPPATH . 'Plugins/CasposPlugin/Language/'); +?> + +
+
+

+
+
+ + +
+ + +
+ +
+ + +
+ + + +
+
+``` + +### Using Language Strings in View Hooks + +```php +addLanguagePath(APPPATH . 'Plugins/CasposPlugin/Language/'); +?> + + +   + + +``` + +### BasePlugin Language Helper + +Add this method to `BasePlugin` to simplify language loading: + +```php +getPluginDir() . '/Language/'; + + if (is_dir($pluginLangPath)) { + $language->addLanguagePath($pluginLangPath); + } + + return $language->getLine($key, $data); + } + + abstract protected function getPluginDir(): string; +} +``` + +### Benefits of Self-Contained Language Files + +1. **Plugin Independence**: No need to modify core language files +2. **Easy Distribution**: Plugin zip includes all translations +3. **Fallback Support**: Missing translations fall back to English +4. **User Contributions**: Users can add translations to `Language/{locale}/` in the plugin directory + +### Language File Naming Convention + +Language files should be named after the plugin class (e.g., `CasposPlugin.php`) to avoid conflicts with core language files and other plugins. + +``` +Language/{locale}/{PluginClass}.php +``` + +This ensures language strings are loaded from the correct plugin's Language directory. + ## Plugin Settings Store plugin-specific settings using: @@ -557,55 +738,6 @@ $this->log('error', 'Error message'); 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/ - │ ├── CasposData.php # Database model - │ └── CasposTransaction.php - ├── Controllers/ - │ ├── Dashboard.php # Admin dashboard - │ └── Settings.php # Settings page - ├── Views/ - │ ├── config.php # Configuration form - │ ├── dashboard.php # Dashboard view - │ └── transaction_list.php - ├── Language/ # Self-contained translations - │ ├── en/ - │ │ └── CasposPlugin.php # English translations - │ ├── es-ES/ - │ │ └── CasposPlugin.php # Spanish translations - │ └── de-DE/ - │ └── CasposPlugin.php # German translations - ├── Libraries/ - │ └── ApiClient.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: @@ -625,296 +757,4 @@ CasposPlugin-1.0.0.zip └── README.md # Plugin documentation ``` -Users extract the zip to `app/Plugins/` and the plugin is ready to use. - -## Internationalization (Language Files) - -Plugins can include their own language files, making them completely self-contained. This allows plugins to provide translations without modifying core language files. - -### Plugin Language Directory Structure - -``` -app/Plugins/ -└── CasposPlugin/ - ├── CasposPlugin.php - ├── Language/ - │ ├── en/ - │ │ └── CasposPlugin.php # English translations - │ ├── es-ES/ - │ │ └── CasposPlugin.php # Spanish translations - │ └── de-DE/ - │ └── CasposPlugin.php # German translations - └── Views/ - └── config.php -``` - -### Language File Format - -Each language file returns an array of translation strings: - -```php - 'CASPOS Integration', - 'caspos_plugin_desc' => 'Azerbaijan government cash register integration', - 'caspos_print_receipt' => 'Print Fiscal Receipt', - 'caspos_fiscal_number' => 'Fiscal Number', - 'caspos_api_url' => 'API URL', - 'caspos_api_key' => 'API Key', - 'caspos_configuration' => 'CASPOS Configuration', - 'caspos_sync_success' => 'Sale synchronized successfully', - 'caspos_sync_failed' => 'Failed to synchronize sale', -]; -``` - -```php - 'Integración CASPOS', - 'caspos_plugin_desc' => 'Integración del registro de efectivo del gobierno de Azerbaiyán', - 'caspos_print_receipt' => 'Imprimir Recibo Fiscal', - 'caspos_fiscal_number' => 'Número Fiscal', - 'caspos_api_url' => 'URL de API', - 'caspos_api_key' => 'Clave de API', - 'caspos_configuration' => 'Configuración CASPOS', - 'caspos_sync_success' => 'Venta sincronizada exitosamente', - 'caspos_sync_failed' => 'Error al sincronizar la venta', -]; -``` - -### Loading Language Strings in Plugins - -The `BasePlugin` class provides a helper method to load plugin-specific language strings: - -```php -lang('caspos_plugin_name'); - } - - public function getPluginDescription(): string - { - return $this->lang('caspos_plugin_desc'); - } - - public function onItemSale(array $saleData): void - { - if (!$this->isEnabled()) { - return; - } - - $result = $this->sendToApi($saleData); - - if ($result['success']) { - $this->log('info', $this->lang('caspos_sync_success')); - } else { - $this->log('error', $this->lang('caspos_sync_failed') . ': ' . $result['error']); - } - } - - protected function lang(string $key, array $data = []): string - { - $language = \Config\Services::language(); - $language->addLanguagePath(APPPATH . 'Plugins/CasposPlugin/Language/'); - return $language->getLine($key, $data); - } -} -``` - -### Using Language Strings in Plugin Views - -```php -addLanguagePath(APPPATH . 'Plugins/CasposPlugin/Language/'); -?> - -
-
-

-
-
- - -
- - -
- -
- - -
- - - -
-
-``` - -### Using Language Strings in View Hooks - -```php -addLanguagePath(APPPATH . 'Plugins/CasposPlugin/Language/'); -?> - - -   - - -``` - -### BasePlugin Language Helper - -Add this method to `BasePlugin` to simplify language loading: - -```php -getPluginDir() . '/Language/'; - - if (is_dir($pluginLangPath)) { - $language->addLanguagePath($pluginLangPath); - } - - return $language->getLine($key, $data); - } - - abstract protected function getPluginDir(): string; -} -``` - -### Benefits of Self-Contained Language Files - -1. **Plugin Independence**: No need to modify core language files -2. **Easy Distribution**: Plugin zip includes all translations -3. **Fallback Support**: Missing translations fall back to English -4. **User Contributions**: Users can add translations to `Language/{locale}/` in the plugin directory - -### Example: Complete Self-Contained Plugin with Languages - -``` -app/Plugins/CasposPlugin/ -├── CasposPlugin.php # Main plugin class -├── Language/ -│ ├── en/ -│ │ └── CasposPlugin.php # English (default) -│ ├── es-ES/ -│ │ └── CasposPlugin.php # Spanish -│ ├── de-DE/ -│ │ └── CasposPlugin.php # German -│ └── az/ -│ └── CasposPlugin.php # Azerbaijani -├── Models/ -│ └── CasposData.php -├── Controllers/ -│ └── Dashboard.php -├── Views/ -│ ├── config.php -│ ├── dashboard.php -│ └── receipt_button.php -└── Libraries/ - └── ApiClient.php -``` - -### Language File Naming Convention - -Language files should be named after the plugin class (e.g., `CasposPlugin.php`) to avoid conflicts with core language files and other plugins. - -``` -Language/{locale}/{PluginClass}.php -``` - -This ensures language strings are loaded from the correct plugin's Language directory. - -## Example: Plugin with View Hooks - -Here's a complete example of a plugin that injects UI elements into core views: - -``` -app/Plugins/CasposPlugin/ -├── CasposPlugin.php # Main class with view hook registration -└── Views/ - ├── receipt_button.php # Button for receipt view - ├── customer_tab.php # Tab for customer form - └── config.php # Plugin configuration -``` - -**Main Plugin Class:** - -```php -isEnabled()) { - return ''; - } - return view('Plugins/CasposPlugin/Views/receipt_button', $data); - } - - public function injectCustomerTab(array $data): string - { - if (!$this->isEnabled()) { - return ''; - } - return view('Plugins/CasposPlugin/Views/customer_tab', $data); - } - - // ... rest of implementation -} -``` - -Core views that want to support plugin hooks: - -```php -// app/Views/sales/receipt.php -
- - $sale]) ?> -
-``` \ No newline at end of file +Users extract the zip to `app/Plugins/` and the plugin is ready to use. \ No newline at end of file