feat(plugins): add view hooks for injecting plugin content into core views

Add event-based view hook system allowing plugins to inject UI elements
into core views without modifying core files. Includes helper functions
and example CasposPlugin demonstrating the pattern.
This commit is contained in:
Ollama committed 2026-03-12 10:13:12 +00:00
1 parent bd8b4fa6c1
commit 66f7d70749
7 files changed
+387 -2

No files matched your search

+2 -1
View File
@@ -205,6 +205,7 @@ class Autoload extends AutoloadConfig
'cookie',
'tabular',
'locale',
'security'
'security',
'plugin'
];
}
+27
View File
@@ -0,0 +1,27 @@
<?php
use CodeIgniter\Events\Events;
if (!function_exists('plugin_content'))
{
function plugin_content(string $section, array $data = []): string
{
$results = Events::trigger("view:{$section}", $data);
if (is_array($results))
{
return implode('', array_filter($results, fn($r) => is_string($r)));
}
return is_string($results) ? $results : '';
}
}
if (!function_exists('plugin_content_exists'))
{
function plugin_content_exists(string $section): bool
{
$observers = Events::listRegistered("view:{$section}");
return !empty($observers);
}
}
+127
View File
@@ -0,0 +1,127 @@
<?php
namespace App\Plugins\CasposPlugin;
use App\Libraries\Plugins\BasePlugin;
use CodeIgniter\Events\Events;
class CasposPlugin extends BasePlugin
{
public function getPluginId(): string
{
return 'caspos';
}
public function getPluginName(): string
{
return 'CASPOS Integration';
}
public function getPluginDescription(): string
{
return 'Azerbaijan government cash register integration for fiscal receipts';
}
public function getVersion(): string
{
return '1.0.0';
}
public function registerEvents(): void
{
Events::on('item_sale', [$this, 'onItemSale']);
Events::on('view:receipt_actions', [$this, 'injectReceiptButton']);
Events::on('view:customer_tabs', [$this, 'injectCustomerTab']);
$this->log('debug', 'CASPOS plugin events registered');
}
public function install(): bool
{
$this->log('info', 'Installing CASPOS plugin');
$this->setSetting('api_url', '');
$this->setSetting('api_key', '');
$this->setSetting('merchant_id', '');
$this->setSetting('show_receipt_button', '1');
return true;
}
public function uninstall(): bool
{
$this->log('info', 'Uninstalling CASPOS plugin');
return true;
}
public function getConfigView(): ?string
{
return 'Plugins/CasposPlugin/Views/config';
}
public function getSettings(): array
{
return [
'api_url' => $this->getSetting('api_url', ''),
'api_key' => $this->getSetting('api_key', ''),
'merchant_id' => $this->getSetting('merchant_id', ''),
'show_receipt_button' => $this->getSetting('show_receipt_button', '1'),
];
}
public function saveSettings(array $settings): bool
{
if (isset($settings['api_url']))
{
$this->setSetting('api_url', $settings['api_url']);
}
if (isset($settings['api_key']))
{
$this->setSetting('api_key', $settings['api_key']);
}
if (isset($settings['merchant_id']))
{
$this->setSetting('merchant_id', $settings['merchant_id']);
}
if (isset($settings['show_receipt_button']))
{
$this->setSetting('show_receipt_button', $settings['show_receipt_button'] ? '1' : '0');
}
return true;
}
public function onItemSale(array $saleData): void
{
if (!$this->isEnabled())
{
return;
}
$this->log('info', "CASPOS: Processing sale {$saleData['sale_id_num']}");
}
public function injectReceiptButton(array $data): string
{
if (!$this->isEnabled() || $this->getSetting('show_receipt_button', '1') !== '1')
{
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);
}
}
+35
View File
@@ -0,0 +1,35 @@
<div class="form-group">
<label class="control-label">
<span class="glyphicon glyphicon-print">&nbsp;</span>
<?= esc(lang('Plugins.caspos_config') ?? 'CASPOS Configuration') ?>
</label>
</div>
<div class="form-group">
<label for="api_url"><?= esc(lang('Plugins.caspos_api_url') ?? 'API URL') ?></label>
<input type="text" class="form-control" name="api_url"
value="<?= esc($settings['api_url'] ?? '') ?>"
placeholder="https://api.caspos.gov.az/v1">
</div>
<div class="form-group">
<label for="api_key"><?= esc(lang('Plugins.caspos_api_key') ?? 'API Key') ?></label>
<input type="text" class="form-control" name="api_key"
value="<?= esc($settings['api_key'] ?? '') ?>">
</div>
<div class="form-group">
<label for="merchant_id"><?= esc(lang('Plugins.caspos_merchant_id') ?? 'Merchant ID') ?></label>
<input type="text" class="form-control" name="merchant_id"
value="<?= esc($settings['merchant_id'] ?? '') ?>">
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" name="show_receipt_button" value="1"
<?= ($settings['show_receipt_button'] ?? '1') === '1' ? 'checked' : '' ?>>
<?= esc(lang('Plugins.caspos_show_button') ?? 'Show fiscal receipt button on receipts') ?>
</label>
</div>
</div>
@@ -0,0 +1,6 @@
<li>
<a href="#caspos_panel" data-toggle="tab">
<span class="glyphicon glyphicon-print">&nbsp;</span>
<?= esc(lang('Plugins.caspos_tab') ?? 'Fiscal Records') ?>
</a>
</li>
@@ -0,0 +1,4 @@
<a href="javascript:void(0);" class="btn btn-info btn-sm" id="show_print_caspos_button" onclick="printCasposReceipt(<?= $sale['sale_id'] ?? '' ?>)">
<span class="glyphicon glyphicon-print">&nbsp;</span>
<?= esc(lang('Plugins.caspos_print_receipt') ?? 'Print Fiscal Receipt') ?>
</a>
+186 -1
View File
@@ -100,6 +100,123 @@ OSPOS fires these events that plugins can listen to:
| `items_csv_import` | `array $importData` | Fired after items CSV import |
| `customers_csv_import` | `array $importData` | Fired after customers CSV import |
## View Hooks (Injecting Plugin Content into Views)
Plugins can inject UI elements into core views using the event-based view hook system. This allows plugins to add buttons, tabs, or other content without modifying core view files.
### How It Works
1. **Core views define hook points** using the `plugin_content()` helper
2. **Plugins register listeners** for these view hooks in `registerEvents()`
3. **Content is conditionally rendered** only if the plugin is enabled
### Step 1: Adding Hook Points in Core Views
In your core view files, use the `plugin_content()` helper to define injection points:
```php
// In app/Views/sales/receipt.php
<div class="receipt-actions">
<!-- Existing buttons -->
<?= plugin_content('receipt_actions', ['sale' => $sale]) ?>
</div>
// In app/Views/customers/form.php
<ul class="nav nav-tabs">
<!-- Existing tabs -->
<?= plugin_content('customer_tabs', ['customer' => $customer]) ?>
</ul>
```
### Step 2: Plugin Registers View Hook
In your plugin class, register a listener that returns HTML content:
```php
class CasposPlugin extends BasePlugin
{
public function registerEvents(): void
{
Events::on('item_sale', [$this, 'onItemSale']);
// View hooks - inject content into core views
Events::on('view:receipt_actions', [$this, 'injectReceiptButton']);
Events::on('view:customer_tabs', [$this, 'injectCustomerTab']);
}
public function injectReceiptButton(array $data): string
{
if (!$this->isEnabled()) {
return '';
}
// Return HTML from plugin's own view file
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);
}
}
```
### Plugin View Files
The plugin's view files are self-contained within the plugin directory:
```php
// app/Plugins/CasposPlugin/Views/receipt_button.php
<a href="javascript:void(0);" class="btn btn-info btn-sm" onclick="printCasposReceipt(<?= $sale['sale_id'] ?>)">
<span class="glyphicon glyphicon-print">&nbsp;</span>
Print Fiscal Receipt
</a>
// app/Plugins/CasposPlugin/Views/customer_tab.php
<li>
<a href="#caspos_panel" data-toggle="tab">
<span class="glyphicon glyphicon-print">&nbsp;</span>
Fiscal Records
</a>
</li>
```
### Helper Functions
The `plugin_helper.php` provides two functions:
```php
// Render plugin content for a hook point
plugin_content(string $section, array $data = []): string
// Check if any plugin has registered for a hook (for conditional rendering)
plugin_content_exists(string $section): bool
```
### Standard Hook Points
Core views should define these standard hook points:
| Hook Name | Location | Usage |
|-----------|----------|-------|
| `view:receipt_actions` | Receipt view action buttons | Add receipt-related buttons |
| `view:customer_tabs` | Customer form tabs | Add customer-related tabs |
| `view:item_form_buttons` | Item form action buttons | Add item-related buttons |
| `view:sales_complete` | Sale complete screen | Post-sale integration UI |
| `view:reports_menu` | Reports menu | Add custom report links |
### Benefits
- **Self-Contained**: Plugin UI stays in plugin directory
- **Conditional**: Only renders when plugin is enabled
- **Data Access**: Pass context (sale, customer, etc.) to plugin views
- **Multiple Plugins**: Multiple plugins can hook the same location
- **Clean Separation**: Core views remain unmodified
## Creating a Plugin
### Simple Plugin (Single File)
@@ -485,4 +602,72 @@ CasposPlugin-1.0.0.zip
└── README.md # Plugin documentation
```
Users extract the zip to `app/Plugins/` and the plugin is ready to use.
Users extract the zip to `app/Plugins/` and the plugin is ready to use.
## 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
<?php
namespace App\Plugins\CasposPlugin;
use App\Libraries\Plugins\BasePlugin;
use CodeIgniter\Events\Events;
class CasposPlugin extends BasePlugin
{
public function getPluginId(): string { return 'caspos'; }
public function getPluginName(): string { return 'CASPOS Integration'; }
public function registerEvents(): void
{
// Data events
Events::on('item_sale', [$this, 'onItemSale']);
// View hooks - inject content into core views
Events::on('view:receipt_actions', [$this, 'injectReceiptButton']);
Events::on('view:customer_tabs', [$this, 'injectCustomerTab']);
}
public function injectReceiptButton(array $data): string
{
if (!$this->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
<div class="receipt-actions">
<?= anchor('sales/reprint/' . $sale_id, 'Reprint', 'class="btn btn-primary"') ?>
<?= plugin_content('view:receipt_actions', ['sale' => $sale]) ?>
</div>
```