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
140 lines
4.7 KiB
Markdown
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`).
|