folderweb/app/docs/plugin-system.md

263 lines
6.1 KiB
Markdown
Raw Normal View History

# Plugin System Reference
## Overview
The framework uses a minimal 3-hook plugin system that allows plugins to modify behavior without the core knowing about them. Plugins are PHP files that register callbacks to specific hooks.
## Hook System
### Available Hooks
```php
enum Hook: string {
case CONTEXT_READY = 'context_ready'; // After context created, before routing
case PROCESS_CONTENT = 'process_content'; // When loading/processing content
case TEMPLATE_VARS = 'template_vars'; // When building template variables
}
```
### Core Functions
```php
// Register a filter
Hooks::add(Hook $hook, callable $callback): void
// Apply filters (modify and return data)
Hooks::apply(Hook $hook, mixed $value, mixed ...$args): mixed
```
## Creating a Plugin
### Plugin Location
```
custom/plugins/global/your-plugin.php # Custom plugins
app/plugins/global/your-plugin.php # Default plugins
```
### Plugin Structure
```php
<?php
// custom/plugins/global/my-plugin.php
// Hook 1: Modify context after creation
Hooks::add(Hook::CONTEXT_READY, function(Context $ctx, array $config) {
// Modify context, add data
$ctx->set('myData', 'value');
return $ctx;
});
// Hook 2: Process content/metadata
Hooks::add(Hook::PROCESS_CONTENT, function(mixed $data, string $context) {
// Modify content files, metadata, etc.
return $data;
});
// Hook 3: Add template variables
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
// Add variables for templates
$vars['myVar'] = 'value';
return $vars;
});
// Helper functions for your plugin
function myHelper() {
// Plugin-specific logic
}
```
### Enable Plugin
Add to `custom/config.ini`:
```ini
[plugins]
enabled = "my-plugin,another-plugin"
```
## Hook Details
### 1. CONTEXT_READY
**When:** After context created, before routing starts
**Purpose:** Modify request handling, extract data from URL, inject context properties
**Signature:** `function(Context $ctx, array $config): Context`
**Example:**
```php
Hooks::add(Hook::CONTEXT_READY, function(Context $ctx, array $config) {
// Extract custom parameter from URL
$parts = explode('/', $ctx->requestPath);
if ($parts[0] === 'api') {
$ctx->set('isApi', true);
array_shift($parts);
// Update request path
$reflection = new ReflectionProperty($ctx, 'requestPath');
$reflection->setValue($ctx, implode('/', $parts));
}
return $ctx;
});
```
### 2. PROCESS_CONTENT
**When:** During content loading and processing
**Purpose:** Filter content files, modify metadata, transform data
**Signature:** `function(mixed $data, string $context, ...): mixed`
**Use Cases:**
**Filter content files:**
```php
Hooks::add(Hook::PROCESS_CONTENT, function(mixed $data, string $dir) {
if (is_array($data) && isset($data[0]['path'])) {
// $data is array of content files
return array_filter($data, fn($file) =>
!str_contains($file['name'], 'draft')
);
}
return $data;
});
```
**Modify metadata:**
```php
Hooks::add(Hook::PROCESS_CONTENT, function(mixed $data, string $dir, string $type = '') {
if ($type === 'metadata') {
// $data is metadata array
$data['processed'] = true;
}
return $data;
});
```
**Format dates:**
```php
Hooks::add(Hook::PROCESS_CONTENT, function(mixed $data, string $type) {
if ($type === 'date_format') {
// $data is date string
return date('F j, Y', strtotime($data));
}
return $data;
});
```
### 3. TEMPLATE_VARS
**When:** Before rendering templates
**Purpose:** Add variables for use in templates
**Signature:** `function(array $vars, Context $ctx): array`
**Example:**
```php
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
$vars['siteName'] = 'My Site';
$vars['year'] = date('Y');
$vars['customData'] = $ctx->get('myData');
return $vars;
});
```
## Context Storage
Plugins can store data in the context using generic storage:
```php
// Set data
$ctx->set('key', $value);
// Get data
$value = $ctx->get('key', $default);
// Check if exists
if ($ctx->has('key')) { }
// Magic property access
$ctx->myKey = 'value';
$value = $ctx->myKey;
```
## Best Practices
1. **Keep plugins self-contained** - All logic in one file
2. **Use namespaced helper functions** - Prefix with plugin name
3. **Store plugin data in context** - Use `$ctx->set()` for plugin state
4. **Return modified data** - Always return from hooks
5. **Use global context when needed** - `$GLOBALS['ctx']` for cross-hook access
6. **Document your hooks** - Comment what each hook does
## Plugin Loading Order
1. Config loaded
2. Global plugins loaded (from config)
3. Context created
4. `CONTEXT_READY` hooks run
5. Routing happens
6. Content loaded
7. `PROCESS_CONTENT` hooks run (multiple times)
8. Template variables prepared
9. `TEMPLATE_VARS` hooks run
10. Template rendered
## Example: Complete Plugin
```php
<?php
// custom/plugins/global/analytics.php
// Add analytics configuration to context
Hooks::add(Hook::CONTEXT_READY, function(Context $ctx, array $config) {
$analyticsId = $config['analytics']['id'] ?? null;
$ctx->set('analyticsId', $analyticsId);
return $ctx;
});
// Add analytics variables to templates
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
$vars['analyticsId'] = $ctx->get('analyticsId');
$vars['analyticsEnabled'] = !empty($vars['analyticsId']);
return $vars;
});
```
Config:
```ini
[analytics]
id = "G-XXXXXXXXXX"
[plugins]
enabled = "analytics"
```
Template usage:
```php
<?php if ($analyticsEnabled): ?>
<script async src="https://www.googletagmanager.com/gtag/js?id=<?= $analyticsId ?>"></script>
<?php endif; ?>
```
## Debugging
Check loaded plugins:
```php
$plugins = getPluginManager()->getLoadedPlugins();
var_dump($plugins);
```
Check if plugin loaded:
```php
if (getPluginManager()->isLoaded('my-plugin')) {
// Plugin is active
}
```
## Limitations
- No priorities (hooks run in registration order)
- No actions (only filters that return values)
- No unhooking (once registered, always runs)
- Plugins load once per request
For advanced needs, consider multiple plugins or extending the hook system.