folderweb/docs/04-development/05-hooks-plugins.md

141 lines
4.7 KiB
Markdown
Raw Permalink Normal View History

# 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`).