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

4.7 KiB

Hooks & Plugins

Hook System

Defined in app/hooks.php. Two constructs:

Hook Enum

enum Hook: string {
    case CONTEXT_READY = 'context_ready';
    case PROCESS_CONTENT = 'process_content';
    case TEMPLATE_VARS = 'template_vars';
}

Hooks Class

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

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

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
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:

[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

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 = valsection.key).