folderweb/app/docs/plugin-system.md
Ruben a205f2cbd7 Remove language-specific content handling
Refactor to use plugin system for language support

Remove hardcoded language features from core

Move language handling to plugin system

Improve content file discovery

Simplify context creation

Add plugin system documentation

Implement hook system for extensibility

Add template variable hook

Add context storage for plugins

Improve error handling

Refactor rendering logic

Improve list view sorting

Add support for custom list templates

Improve metadata handling

Add plugin system reference documentation
2025-11-25 20:19:12 +01:00

6.1 KiB

Plugin System Reference

Overview

The framework uses a minimal 3-hook plugin system that allows plugins to modify behavior without the core knowing about them. Plugins are PHP files that register callbacks to specific hooks.

Hook System

Available Hooks

enum Hook: string {
    case CONTEXT_READY = 'context_ready';    // After context created, before routing
    case PROCESS_CONTENT = 'process_content'; // When loading/processing content
    case TEMPLATE_VARS = 'template_vars';     // When building template variables
}

Core Functions

// Register a filter
Hooks::add(Hook $hook, callable $callback): void

// Apply filters (modify and return data)
Hooks::apply(Hook $hook, mixed $value, mixed ...$args): mixed

Creating a Plugin

Plugin Location

custom/plugins/global/your-plugin.php    # Custom plugins
app/plugins/global/your-plugin.php       # Default plugins

Plugin Structure

<?php
// custom/plugins/global/my-plugin.php

// Hook 1: Modify context after creation
Hooks::add(Hook::CONTEXT_READY, function(Context $ctx, array $config) {
    // Modify context, add data
    $ctx->set('myData', 'value');
    return $ctx;
});

// Hook 2: Process content/metadata
Hooks::add(Hook::PROCESS_CONTENT, function(mixed $data, string $context) {
    // Modify content files, metadata, etc.
    return $data;
});

// Hook 3: Add template variables
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
    // Add variables for templates
    $vars['myVar'] = 'value';
    return $vars;
});

// Helper functions for your plugin
function myHelper() {
    // Plugin-specific logic
}

Enable Plugin

Add to custom/config.ini:

[plugins]
enabled = "my-plugin,another-plugin"

Hook Details

1. CONTEXT_READY

When: After context created, before routing starts
Purpose: Modify request handling, extract data from URL, inject context properties
Signature: function(Context $ctx, array $config): Context

Example:

Hooks::add(Hook::CONTEXT_READY, function(Context $ctx, array $config) {
    // Extract custom parameter from URL
    $parts = explode('/', $ctx->requestPath);
    if ($parts[0] === 'api') {
        $ctx->set('isApi', true);
        array_shift($parts);
        // Update request path
        $reflection = new ReflectionProperty($ctx, 'requestPath');
        $reflection->setValue($ctx, implode('/', $parts));
    }
    return $ctx;
});

2. PROCESS_CONTENT

When: During content loading and processing
Purpose: Filter content files, modify metadata, transform data
Signature: function(mixed $data, string $context, ...): mixed

Use Cases:

Filter content files:

Hooks::add(Hook::PROCESS_CONTENT, function(mixed $data, string $dir) {
    if (is_array($data) && isset($data[0]['path'])) {
        // $data is array of content files
        return array_filter($data, fn($file) => 
            !str_contains($file['name'], 'draft')
        );
    }
    return $data;
});

Modify metadata:

Hooks::add(Hook::PROCESS_CONTENT, function(mixed $data, string $dir, string $type = '') {
    if ($type === 'metadata') {
        // $data is metadata array
        $data['processed'] = true;
    }
    return $data;
});

Format dates:

Hooks::add(Hook::PROCESS_CONTENT, function(mixed $data, string $type) {
    if ($type === 'date_format') {
        // $data is date string
        return date('F j, Y', strtotime($data));
    }
    return $data;
});

3. TEMPLATE_VARS

When: Before rendering templates
Purpose: Add variables for use in templates
Signature: function(array $vars, Context $ctx): array

Example:

Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
    $vars['siteName'] = 'My Site';
    $vars['year'] = date('Y');
    $vars['customData'] = $ctx->get('myData');
    return $vars;
});

Context Storage

Plugins can store data in the context using generic storage:

// Set data
$ctx->set('key', $value);

// Get data
$value = $ctx->get('key', $default);

// Check if exists
if ($ctx->has('key')) { }

// Magic property access
$ctx->myKey = 'value';
$value = $ctx->myKey;

Best Practices

  1. Keep plugins self-contained - All logic in one file
  2. Use namespaced helper functions - Prefix with plugin name
  3. Store plugin data in context - Use $ctx->set() for plugin state
  4. Return modified data - Always return from hooks
  5. Use global context when needed - $GLOBALS['ctx'] for cross-hook access
  6. Document your hooks - Comment what each hook does

Plugin Loading Order

  1. Config loaded
  2. Global plugins loaded (from config)
  3. Context created
  4. CONTEXT_READY hooks run
  5. Routing happens
  6. Content loaded
  7. PROCESS_CONTENT hooks run (multiple times)
  8. Template variables prepared
  9. TEMPLATE_VARS hooks run
  10. Template rendered

Example: Complete Plugin

<?php
// custom/plugins/global/analytics.php

// Add analytics configuration to context
Hooks::add(Hook::CONTEXT_READY, function(Context $ctx, array $config) {
    $analyticsId = $config['analytics']['id'] ?? null;
    $ctx->set('analyticsId', $analyticsId);
    return $ctx;
});

// Add analytics variables to templates
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
    $vars['analyticsId'] = $ctx->get('analyticsId');
    $vars['analyticsEnabled'] = !empty($vars['analyticsId']);
    return $vars;
});

Config:

[analytics]
id = "G-XXXXXXXXXX"

[plugins]
enabled = "analytics"

Template usage:

<?php if ($analyticsEnabled): ?>
    <script async src="https://www.googletagmanager.com/gtag/js?id=<?= $analyticsId ?>"></script>
<?php endif; ?>

Debugging

Check loaded plugins:

$plugins = getPluginManager()->getLoadedPlugins();
var_dump($plugins);

Check if plugin loaded:

if (getPluginManager()->isLoaded('my-plugin')) {
    // Plugin is active
}

Limitations

  • No priorities (hooks run in registration order)
  • No actions (only filters that return values)
  • No unhooking (once registered, always runs)
  • Plugins load once per request

For advanced needs, consider multiple plugins or extending the hook system.