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