141 lines
4.7 KiB
Markdown
141 lines
4.7 KiB
Markdown
|
|
# Hooks & Plugins
|
||
|
|
|
||
|
|
## Hook System
|
||
|
|
|
||
|
|
Defined in `app/hooks.php`. Two constructs:
|
||
|
|
|
||
|
|
### Hook Enum
|
||
|
|
|
||
|
|
```php
|
||
|
|
enum Hook: string {
|
||
|
|
case CONTEXT_READY = 'context_ready';
|
||
|
|
case PROCESS_CONTENT = 'process_content';
|
||
|
|
case TEMPLATE_VARS = 'template_vars';
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Hooks Class
|
||
|
|
|
||
|
|
```php
|
||
|
|
Hooks::add(Hook $hook, callable $callback): void
|
||
|
|
Hooks::apply(Hook $hook, mixed $value, mixed ...$args): mixed
|
||
|
|
```
|
||
|
|
|
||
|
|
`apply` chains all registered callbacks — each receives the return value of the previous. **Callbacks must return the modified value** or the chain breaks.
|
||
|
|
|
||
|
|
## The Three Hooks
|
||
|
|
|
||
|
|
### Hook::CONTEXT_READY
|
||
|
|
|
||
|
|
**When:** After `Context` creation, before routing.
|
||
|
|
**Signature:** `function(Context $ctx, array $config): Context`
|
||
|
|
**Use:** Set context values, process config, modify request state.
|
||
|
|
**Must return:** `$ctx`
|
||
|
|
|
||
|
|
### Hook::PROCESS_CONTENT
|
||
|
|
|
||
|
|
**When:** During content loading — files, metadata, dates.
|
||
|
|
**Signature:** `function(mixed $data, string $dirOrType, string $extraContext = ''): mixed`
|
||
|
|
**Must return:** Modified `$data`
|
||
|
|
|
||
|
|
Called in these contexts:
|
||
|
|
|
||
|
|
| `$data` type | `$dirOrType` | `$extraContext` | Purpose |
|
||
|
|
|---|---|---|---|
|
||
|
|
| array of `['path','name','ext']` | directory path | `""` | Filter content files (e.g., by language) |
|
||
|
|
| array (metadata) | directory path | `"metadata"` | Transform metadata (e.g., merge language sections) |
|
||
|
|
| string (date) | `"date_format"` | `""` | Format date string |
|
||
|
|
|
||
|
|
**Warning:** The second parameter is overloaded — it is a directory path in the first two cases but a type identifier string in the third. Plugins must distinguish these cases carefully. Use `$extraContext === 'metadata'` to detect metadata processing, and `$dirOrType === 'date_format'` to detect date formatting. In all other cases, `$dirOrType` is a directory path and `$data` is the file list array.
|
||
|
|
|
||
|
|
### Hook::TEMPLATE_VARS
|
||
|
|
|
||
|
|
**When:** Before rendering any template (page or list).
|
||
|
|
**Signature:** `function(array $vars, Context $ctx): array`
|
||
|
|
**Must return:** Modified `$vars` array
|
||
|
|
|
||
|
|
This is the primary extension point for adding custom template variables.
|
||
|
|
|
||
|
|
## Plugin Manager
|
||
|
|
|
||
|
|
Defined in `app/plugins.php`. Singleton via `getPluginManager()`.
|
||
|
|
|
||
|
|
### Loading
|
||
|
|
|
||
|
|
```php
|
||
|
|
getPluginManager()->loadGlobalPlugins(array $config) // Called during createContext()
|
||
|
|
getPluginManager()->loadPagePlugins(?array $metadata) // Called before rendering
|
||
|
|
```
|
||
|
|
|
||
|
|
**Global plugins:** Loaded from config `[plugins] enabled = "name1,name2"`.
|
||
|
|
**Page plugins:** Loaded from metadata `plugins = "name1,name2"`.
|
||
|
|
|
||
|
|
### Resolution Order
|
||
|
|
|
||
|
|
For each plugin name, checks:
|
||
|
|
1. `custom/plugins/{scope}/{name}.php`
|
||
|
|
2. `app/plugins/{scope}/{name}.php`
|
||
|
|
|
||
|
|
Custom wins. Each plugin is loaded once (`require_once`).
|
||
|
|
|
||
|
|
### Introspection
|
||
|
|
|
||
|
|
```php
|
||
|
|
getPluginManager()->getLoadedPlugins(): array // Names of all loaded plugins
|
||
|
|
getPluginManager()->isLoaded(string $name): bool
|
||
|
|
getPluginManager()->getPluginInfo(string $name): ?array // ['path','scope','loaded_at']
|
||
|
|
```
|
||
|
|
|
||
|
|
## Writing a Plugin
|
||
|
|
|
||
|
|
Create `custom/plugins/global/{name}.php`:
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx): array {
|
||
|
|
if (isset($vars['content'])) {
|
||
|
|
$words = str_word_count(strip_tags($vars['content']));
|
||
|
|
$vars['readingTime'] = max(1, round($words / 200));
|
||
|
|
}
|
||
|
|
return $vars;
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
Enable in `custom/config.ini`:
|
||
|
|
|
||
|
|
```ini
|
||
|
|
[plugins]
|
||
|
|
enabled = "languages,your-plugin-name"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Plugin Rules
|
||
|
|
|
||
|
|
1. **Always return** the modified value from hook callbacks
|
||
|
|
2. **Prefix helper functions** to avoid collisions (`myPlugin_helper()`)
|
||
|
|
3. **Read config** via the `$config` parameter in `CONTEXT_READY`, not by reading files
|
||
|
|
4. **Check variable existence** before using (`isset()`, `??`)
|
||
|
|
5. **No inter-plugin communication** — plugins are independent
|
||
|
|
6. **Execution order** follows registration order (load order), no priority system
|
||
|
|
|
||
|
|
### Accessing Config from Plugins
|
||
|
|
|
||
|
|
```php
|
||
|
|
Hooks::add(Hook::CONTEXT_READY, function(Context $ctx, array $config): Context {
|
||
|
|
$myVal = $config['my_plugin']['key'] ?? 'default';
|
||
|
|
$ctx->set('my_key', $myVal);
|
||
|
|
return $ctx;
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Built-in Plugin: languages.php
|
||
|
|
|
||
|
|
The language plugin registers all three hooks:
|
||
|
|
|
||
|
|
- **CONTEXT_READY:** Extracts language from URL prefix, sets `currentLang`/`defaultLang`/`langPrefix`/`translations`/`availableLangs` on context
|
||
|
|
- **PROCESS_CONTENT:** Filters content files by language variant (`name.lang.ext`), merges language metadata sections, formats dates with translated month names
|
||
|
|
- **TEMPLATE_VARS:** Exposes language variables and `$languageUrls` to templates
|
||
|
|
|
||
|
|
Language file naming: `name.lang.ext` (e.g., `index.no.md`). A 2-letter code before the extension triggers language filtering.
|
||
|
|
|
||
|
|
Translation files: `custom/languages/{lang}.ini` merged over `app/default/languages/{lang}.ini`. Supports INI sections flattened to dot notation (`[section] key = val` → `section.key`).
|