folderweb/docs/04-development/05-hooks-plugins.md
Ruben b03511f99b Update AGENT.md and add architecture documentation
Update AGENT.md to reflect current project structure and philosophy

Add comprehensive architecture documentation covering:
- Directory layout
- Stable contracts
- Request flow
- Module dependencies
- Page vs list detection
- Deployment models
- Demo fallback

Remove outdated plugin system documentation
Add new content system documentation
Add configuration documentation
Add context API documentation
Add hooks and plugins documentation
Add templates documentation
Add rendering documentation
Add development environment documentation
2026-02-05 23:30:44 +01:00

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