649 lines
15 KiB
Markdown
649 lines
15 KiB
Markdown
|
|
# Plugin System
|
||
|
|
|
||
|
|
FolderWeb uses a minimal hook-based plugin system for extensibility. Plugins let you modify content, add functionality, and inject custom variables into templates—all without touching the framework code.
|
||
|
|
|
||
|
|
## How Plugins Work
|
||
|
|
|
||
|
|
Plugins are PHP files that register callbacks with one or more **hooks**:
|
||
|
|
|
||
|
|
1. **`Hook::CONTEXT_READY`** — After context is created, before routing
|
||
|
|
2. **`Hook::PROCESS_CONTENT`** — When loading/processing content
|
||
|
|
3. **`Hook::TEMPLATE_VARS`** — Before rendering templates
|
||
|
|
|
||
|
|
Each hook receives data, allows modification, and returns the modified data.
|
||
|
|
|
||
|
|
## Plugin Locations
|
||
|
|
|
||
|
|
```
|
||
|
|
app/plugins/
|
||
|
|
├── global/ # Built-in global plugins (don't modify)
|
||
|
|
│ └── languages.php
|
||
|
|
└── page/ # Built-in page plugins (empty by default)
|
||
|
|
|
||
|
|
custom/plugins/
|
||
|
|
├── global/ # Your global plugins
|
||
|
|
│ ├── analytics.php
|
||
|
|
│ └── reading-time.php
|
||
|
|
└── page/ # Your page plugins (not yet used)
|
||
|
|
```
|
||
|
|
|
||
|
|
**Global plugins:** Loaded on every request
|
||
|
|
**Page plugins:** Reserved for future use
|
||
|
|
|
||
|
|
## Enabling Plugins
|
||
|
|
|
||
|
|
List enabled plugins in `custom/config.ini`:
|
||
|
|
|
||
|
|
```ini
|
||
|
|
[plugins]
|
||
|
|
enabled = "languages,analytics,reading-time"
|
||
|
|
```
|
||
|
|
|
||
|
|
Plugin names correspond to filenames without `.php`:
|
||
|
|
- `languages` → `languages.php`
|
||
|
|
- `analytics` → `analytics.php`
|
||
|
|
- `reading-time` → `reading-time.php`
|
||
|
|
|
||
|
|
FolderWeb loads plugins from:
|
||
|
|
1. `app/plugins/global/` (built-in)
|
||
|
|
2. `custom/plugins/global/` (yours)
|
||
|
|
|
||
|
|
## The Three Hooks
|
||
|
|
|
||
|
|
### `Hook::CONTEXT_READY`
|
||
|
|
|
||
|
|
Called after the context object is created, before routing begins.
|
||
|
|
|
||
|
|
**Use for:**
|
||
|
|
- Setting global context values
|
||
|
|
- Processing configuration
|
||
|
|
- Adding cross-cutting concerns
|
||
|
|
|
||
|
|
**Signature:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
Hooks::add(Hook::CONTEXT_READY, function(Context $ctx, array $config) {
|
||
|
|
// Modify context
|
||
|
|
$ctx->set('key', 'value');
|
||
|
|
|
||
|
|
// Must return context
|
||
|
|
return $ctx;
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
**Parameters:**
|
||
|
|
- `$ctx` — Context object (see [Context API](#context-api))
|
||
|
|
- `$config` — Merged configuration array from `config.ini`
|
||
|
|
|
||
|
|
**Must return:** Modified `$ctx`
|
||
|
|
|
||
|
|
### `Hook::PROCESS_CONTENT`
|
||
|
|
|
||
|
|
Called when loading or processing content (files, metadata, dates).
|
||
|
|
|
||
|
|
**Use for:**
|
||
|
|
- Filtering content files
|
||
|
|
- Transforming metadata
|
||
|
|
- Custom content processing
|
||
|
|
|
||
|
|
**Signature:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
Hooks::add(Hook::PROCESS_CONTENT, function(mixed $data, string $dirOrType, string $extraContext = '') {
|
||
|
|
// Process data based on type
|
||
|
|
if ($extraContext === 'metadata') {
|
||
|
|
// Modify metadata array
|
||
|
|
$data['custom_field'] = 'value';
|
||
|
|
}
|
||
|
|
|
||
|
|
// Must return data
|
||
|
|
return $data;
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
**Parameters:**
|
||
|
|
- `$data` — The data being processed (type varies)
|
||
|
|
- `$dirOrType` — Directory path or processing type
|
||
|
|
- `$extraContext` — Additional context (e.g., `"metadata"`, `"date_format"`)
|
||
|
|
|
||
|
|
**Must return:** Modified `$data`
|
||
|
|
|
||
|
|
**Common `$extraContext` values:**
|
||
|
|
- `"metadata"` — Processing metadata array
|
||
|
|
- `"date_format"` — Formatting a date string
|
||
|
|
|
||
|
|
### `Hook::TEMPLATE_VARS`
|
||
|
|
|
||
|
|
Called before rendering templates, allowing you to add variables.
|
||
|
|
|
||
|
|
**Use for:**
|
||
|
|
- Adding custom template variables
|
||
|
|
- Computing values for display
|
||
|
|
- Injecting data into templates
|
||
|
|
|
||
|
|
**Signature:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||
|
|
// Add custom variables
|
||
|
|
$vars['siteName'] = 'My Website';
|
||
|
|
$vars['currentYear'] = date('Y');
|
||
|
|
|
||
|
|
// Must return vars
|
||
|
|
return $vars;
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
**Parameters:**
|
||
|
|
- `$vars` — Array of template variables
|
||
|
|
- `$ctx` — Context object
|
||
|
|
|
||
|
|
**Must return:** Modified `$vars` array
|
||
|
|
|
||
|
|
## Context API
|
||
|
|
|
||
|
|
The `Context` object stores global state. Access it in hooks:
|
||
|
|
|
||
|
|
```php
|
||
|
|
// Set a value
|
||
|
|
$ctx->set('key', 'value');
|
||
|
|
|
||
|
|
// Get a value
|
||
|
|
$value = $ctx->get('key');
|
||
|
|
|
||
|
|
// Get with default
|
||
|
|
$value = $ctx->get('key', 'default');
|
||
|
|
|
||
|
|
// Check if exists
|
||
|
|
if ($ctx->has('key')) {
|
||
|
|
// ...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Built-in context values:**
|
||
|
|
|
||
|
|
| Key | Type | Description |
|
||
|
|
|-----|------|-------------|
|
||
|
|
| `requestPath` | String | URL path (e.g., `"blog/my-post"`) |
|
||
|
|
| `contentDir` | String | Filesystem path to content |
|
||
|
|
| `currentLang` | String | Current language (from languages plugin) |
|
||
|
|
| `defaultLang` | String | Default language |
|
||
|
|
| `translations` | Array | Translated strings |
|
||
|
|
| `metadata` | Array | Current page metadata |
|
||
|
|
|
||
|
|
## Creating Your First Plugin
|
||
|
|
|
||
|
|
Let's create a plugin that adds a reading time estimate to posts.
|
||
|
|
|
||
|
|
### Step 1: Create the Plugin File
|
||
|
|
|
||
|
|
**custom/plugins/global/reading-time.php:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
|
||
|
|
// Add reading time to template variables
|
||
|
|
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||
|
|
// Only calculate if we have content
|
||
|
|
if (isset($vars['content'])) {
|
||
|
|
$wordCount = str_word_count(strip_tags($vars['content']));
|
||
|
|
$wordsPerMinute = 200;
|
||
|
|
$readingTime = max(1, round($wordCount / $wordsPerMinute));
|
||
|
|
|
||
|
|
$vars['readingTime'] = $readingTime;
|
||
|
|
$vars['wordCount'] = $wordCount;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $vars;
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Step 2: Enable the Plugin
|
||
|
|
|
||
|
|
**custom/config.ini:**
|
||
|
|
|
||
|
|
```ini
|
||
|
|
[plugins]
|
||
|
|
enabled = "languages,reading-time"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Step 3: Use in Template
|
||
|
|
|
||
|
|
**custom/templates/page.php:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
<article>
|
||
|
|
<header>
|
||
|
|
<h1><?= htmlspecialchars($metadata['title'] ?? 'Untitled') ?></h1>
|
||
|
|
|
||
|
|
<?php if (isset($readingTime)): ?>
|
||
|
|
<p class="reading-time">
|
||
|
|
<?= $readingTime ?> min read (<?= number_format($wordCount) ?> words)
|
||
|
|
</p>
|
||
|
|
<?php endif; ?>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<div class="content">
|
||
|
|
<?= $content ?>
|
||
|
|
</div>
|
||
|
|
</article>
|
||
|
|
```
|
||
|
|
|
||
|
|
Done! Every page now shows reading time.
|
||
|
|
|
||
|
|
## Plugin Examples
|
||
|
|
|
||
|
|
### Analytics Plugin
|
||
|
|
|
||
|
|
Add Google Analytics tracking ID to all pages.
|
||
|
|
|
||
|
|
**custom/plugins/global/analytics.php:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
|
||
|
|
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||
|
|
global $config;
|
||
|
|
|
||
|
|
// Read tracking ID from config
|
||
|
|
$trackingId = $config['analytics']['tracking_id'] ?? null;
|
||
|
|
|
||
|
|
if ($trackingId) {
|
||
|
|
$vars['analyticsId'] = $trackingId;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $vars;
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
**custom/config.ini:**
|
||
|
|
|
||
|
|
```ini
|
||
|
|
[analytics]
|
||
|
|
tracking_id = "G-XXXXXXXXXX"
|
||
|
|
|
||
|
|
[plugins]
|
||
|
|
enabled = "languages,analytics"
|
||
|
|
```
|
||
|
|
|
||
|
|
**custom/templates/base.php:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
<head>
|
||
|
|
<!-- ... -->
|
||
|
|
|
||
|
|
<?php if (isset($analyticsId)): ?>
|
||
|
|
<script async src="https://www.googletagmanager.com/gtag/js?id=<?= htmlspecialchars($analyticsId) ?>"></script>
|
||
|
|
<script>
|
||
|
|
window.dataLayer = window.dataLayer || [];
|
||
|
|
function gtag(){dataLayer.push(arguments);}
|
||
|
|
gtag('js', new Date());
|
||
|
|
gtag('config', '<?= htmlspecialchars($analyticsId) ?>');
|
||
|
|
</script>
|
||
|
|
<?php endif; ?>
|
||
|
|
</head>
|
||
|
|
```
|
||
|
|
|
||
|
|
### Table of Contents Plugin
|
||
|
|
|
||
|
|
Generate a table of contents from headings.
|
||
|
|
|
||
|
|
**custom/plugins/global/toc.php:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
|
||
|
|
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||
|
|
if (!isset($vars['content'])) {
|
||
|
|
return $vars;
|
||
|
|
}
|
||
|
|
|
||
|
|
$content = $vars['content'];
|
||
|
|
$toc = [];
|
||
|
|
|
||
|
|
// Extract headings
|
||
|
|
if (preg_match_all('/<h([2-3])>(.*?)<\/h\1>/i', $content, $matches)) {
|
||
|
|
foreach ($matches[0] as $i => $match) {
|
||
|
|
$level = (int)$matches[1][$i];
|
||
|
|
$text = strip_tags($matches[2][$i]);
|
||
|
|
$id = slugify($text);
|
||
|
|
|
||
|
|
// Add ID to heading
|
||
|
|
$newHeading = str_replace('<h' . $level . '>', '<h' . $level . ' id="' . $id . '">', $match);
|
||
|
|
$content = str_replace($match, $newHeading, $content);
|
||
|
|
|
||
|
|
$toc[] = [
|
||
|
|
'level' => $level,
|
||
|
|
'text' => $text,
|
||
|
|
'id' => $id,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$vars['content'] = $content;
|
||
|
|
$vars['tableOfContents'] = $toc;
|
||
|
|
|
||
|
|
return $vars;
|
||
|
|
});
|
||
|
|
|
||
|
|
function slugify(string $text): string {
|
||
|
|
$text = strtolower($text);
|
||
|
|
$text = preg_replace('/[^a-z0-9]+/', '-', $text);
|
||
|
|
return trim($text, '-');
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Use in template:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php if (!empty($tableOfContents)): ?>
|
||
|
|
<nav class="toc">
|
||
|
|
<h2>Table of Contents</h2>
|
||
|
|
<ul>
|
||
|
|
<?php foreach ($tableOfContents as $item): ?>
|
||
|
|
<li class="toc-level-<?= $item['level'] ?>">
|
||
|
|
<a href="#<?= htmlspecialchars($item['id']) ?>">
|
||
|
|
<?= htmlspecialchars($item['text']) ?>
|
||
|
|
</a>
|
||
|
|
</li>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</ul>
|
||
|
|
</nav>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<article>
|
||
|
|
<?= $content ?>
|
||
|
|
</article>
|
||
|
|
```
|
||
|
|
|
||
|
|
### Author Bio Plugin
|
||
|
|
|
||
|
|
Add author information from metadata.
|
||
|
|
|
||
|
|
**custom/plugins/global/author-bio.php:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
|
||
|
|
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||
|
|
$metadata = $ctx->get('metadata', []);
|
||
|
|
|
||
|
|
// Load author data if specified
|
||
|
|
if (isset($metadata['author'])) {
|
||
|
|
$authorSlug = slugify($metadata['author']);
|
||
|
|
$authorFile = dirname(__DIR__, 2) . "/content/authors/$authorSlug.ini";
|
||
|
|
|
||
|
|
if (file_exists($authorFile)) {
|
||
|
|
$authorData = parse_ini_file($authorFile);
|
||
|
|
$vars['authorBio'] = $authorData;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $vars;
|
||
|
|
});
|
||
|
|
|
||
|
|
function slugify(string $text): string {
|
||
|
|
return strtolower(preg_replace('/[^a-z0-9]+/', '-', $text));
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**content/authors/jane-doe.ini:**
|
||
|
|
|
||
|
|
```ini
|
||
|
|
name = "Jane Doe"
|
||
|
|
bio = "Web developer and writer"
|
||
|
|
email = "jane@example.com"
|
||
|
|
twitter = "@janedoe"
|
||
|
|
website = "https://janedoe.com"
|
||
|
|
```
|
||
|
|
|
||
|
|
**Use in template:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php if (isset($authorBio)): ?>
|
||
|
|
<aside class="author-bio">
|
||
|
|
<h3><?= htmlspecialchars($authorBio['name'] ?? 'Unknown') ?></h3>
|
||
|
|
<p><?= htmlspecialchars($authorBio['bio'] ?? '') ?></p>
|
||
|
|
|
||
|
|
<?php if (isset($authorBio['website'])): ?>
|
||
|
|
<a href="<?= htmlspecialchars($authorBio['website']) ?>">Website</a>
|
||
|
|
<?php endif; ?>
|
||
|
|
</aside>
|
||
|
|
<?php endif; ?>
|
||
|
|
```
|
||
|
|
|
||
|
|
### Related Posts Plugin
|
||
|
|
|
||
|
|
Show related posts based on tags.
|
||
|
|
|
||
|
|
**custom/plugins/global/related-posts.php:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
|
||
|
|
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||
|
|
$metadata = $ctx->get('metadata', []);
|
||
|
|
|
||
|
|
// Only for pages with tags
|
||
|
|
if (!isset($metadata['tags'])) {
|
||
|
|
return $vars;
|
||
|
|
}
|
||
|
|
|
||
|
|
$currentPath = $ctx->get('currentPath', '');
|
||
|
|
$currentTags = array_map('trim', explode(',', $metadata['tags']));
|
||
|
|
|
||
|
|
// Find other posts with similar tags
|
||
|
|
$contentDir = $ctx->contentDir;
|
||
|
|
$relatedPosts = findRelatedPosts($contentDir, $currentPath, $currentTags);
|
||
|
|
|
||
|
|
if (!empty($relatedPosts)) {
|
||
|
|
$vars['relatedPosts'] = $relatedPosts;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $vars;
|
||
|
|
});
|
||
|
|
|
||
|
|
function findRelatedPosts(string $contentDir, string $currentPath, array $currentTags): array {
|
||
|
|
$posts = [];
|
||
|
|
|
||
|
|
// Recursively scan content directory
|
||
|
|
$iterator = new RecursiveIteratorIterator(
|
||
|
|
new RecursiveDirectoryIterator($contentDir, RecursiveDirectoryIterator::SKIP_DOTS)
|
||
|
|
);
|
||
|
|
|
||
|
|
foreach ($iterator as $file) {
|
||
|
|
if ($file->getFilename() === 'metadata.ini') {
|
||
|
|
$dir = dirname($file->getPathname());
|
||
|
|
|
||
|
|
// Skip current page
|
||
|
|
if ($dir === $currentPath) continue;
|
||
|
|
|
||
|
|
$metadata = parse_ini_file($file->getPathname());
|
||
|
|
|
||
|
|
if (isset($metadata['tags'])) {
|
||
|
|
$tags = array_map('trim', explode(',', $metadata['tags']));
|
||
|
|
$commonTags = array_intersect($currentTags, $tags);
|
||
|
|
|
||
|
|
if (!empty($commonTags)) {
|
||
|
|
$posts[] = [
|
||
|
|
'title' => $metadata['title'] ?? basename($dir),
|
||
|
|
'url' => str_replace($contentDir, '', $dir) . '/',
|
||
|
|
'tags' => $tags,
|
||
|
|
'relevance' => count($commonTags),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sort by relevance
|
||
|
|
usort($posts, fn($a, $b) => $b['relevance'] <=> $a['relevance']);
|
||
|
|
|
||
|
|
// Return top 3
|
||
|
|
return array_slice($posts, 0, 3);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Use in template:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php if (!empty($relatedPosts)): ?>
|
||
|
|
<aside class="related-posts">
|
||
|
|
<h3>Related Posts</h3>
|
||
|
|
<ul>
|
||
|
|
<?php foreach ($relatedPosts as $post): ?>
|
||
|
|
<li>
|
||
|
|
<a href="<?= htmlspecialchars($post['url']) ?>">
|
||
|
|
<?= htmlspecialchars($post['title']) ?>
|
||
|
|
</a>
|
||
|
|
</li>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</ul>
|
||
|
|
</aside>
|
||
|
|
<?php endif; ?>
|
||
|
|
```
|
||
|
|
|
||
|
|
## Best Practices
|
||
|
|
|
||
|
|
### 1. Always Return Modified Data
|
||
|
|
|
||
|
|
```php
|
||
|
|
// Good
|
||
|
|
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||
|
|
$vars['custom'] = 'value';
|
||
|
|
return $vars; // Always return
|
||
|
|
});
|
||
|
|
|
||
|
|
// Bad
|
||
|
|
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||
|
|
$vars['custom'] = 'value';
|
||
|
|
// Missing return - breaks other plugins!
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Use Configuration for Settings
|
||
|
|
|
||
|
|
```php
|
||
|
|
// Good: configurable
|
||
|
|
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||
|
|
global $config;
|
||
|
|
|
||
|
|
$wordsPerMinute = $config['reading_time']['words_per_minute'] ?? 200;
|
||
|
|
// Use $wordsPerMinute...
|
||
|
|
|
||
|
|
return $vars;
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
**custom/config.ini:**
|
||
|
|
|
||
|
|
```ini
|
||
|
|
[reading_time]
|
||
|
|
words_per_minute = 250
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Check Variable Existence
|
||
|
|
|
||
|
|
```php
|
||
|
|
// Good: defensive
|
||
|
|
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||
|
|
if (isset($vars['content'])) {
|
||
|
|
// Process content
|
||
|
|
}
|
||
|
|
return $vars;
|
||
|
|
});
|
||
|
|
|
||
|
|
// Bad: assumes content exists
|
||
|
|
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||
|
|
$wordCount = str_word_count($vars['content']); // May error
|
||
|
|
return $vars;
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. Namespace Helper Functions
|
||
|
|
|
||
|
|
```php
|
||
|
|
// Good: prefixed function name
|
||
|
|
function readingTime_calculate(string $content): int {
|
||
|
|
// ...
|
||
|
|
}
|
||
|
|
|
||
|
|
// Bad: generic name (may conflict)
|
||
|
|
function calculate(string $content): int {
|
||
|
|
// ...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5. Use Type Hints
|
||
|
|
|
||
|
|
```php
|
||
|
|
// Good: type hints for clarity
|
||
|
|
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx): array {
|
||
|
|
$vars['custom'] = 'value';
|
||
|
|
return $vars;
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## Debugging Plugins
|
||
|
|
|
||
|
|
### Check Plugin Loading
|
||
|
|
|
||
|
|
Add debug output to verify your plugin loads:
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
error_log("My plugin loaded!");
|
||
|
|
|
||
|
|
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||
|
|
error_log("TEMPLATE_VARS hook called");
|
||
|
|
error_log("Variables: " . print_r(array_keys($vars), true));
|
||
|
|
|
||
|
|
$vars['debug'] = 'Plugin is working';
|
||
|
|
return $vars;
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
Check the error log:
|
||
|
|
```bash
|
||
|
|
tail -f /var/log/apache2/error.log
|
||
|
|
```
|
||
|
|
|
||
|
|
### Inspect Hook Order
|
||
|
|
|
||
|
|
Hooks execute in the order they're registered. Check order by logging:
|
||
|
|
|
||
|
|
```php
|
||
|
|
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||
|
|
error_log("Hook 1: " . json_encode(array_keys($vars)));
|
||
|
|
return $vars;
|
||
|
|
});
|
||
|
|
|
||
|
|
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||
|
|
error_log("Hook 2: " . json_encode(array_keys($vars)));
|
||
|
|
return $vars;
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Dump Variables in Templates
|
||
|
|
|
||
|
|
Temporarily add to your template:
|
||
|
|
|
||
|
|
```php
|
||
|
|
<pre><?php var_dump($customVariable); ?></pre>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Remove before deploying to production.**
|
||
|
|
|
||
|
|
## Limitations
|
||
|
|
|
||
|
|
- **No inter-plugin communication:** Plugins can't directly call each other
|
||
|
|
- **Single execution order:** Hooks execute in registration order (no priority system)
|
||
|
|
- **Global scope:** Be careful with global variables and function names
|
||
|
|
- **No automatic loading:** Plugins must be listed in `config.ini`
|
||
|
|
|
||
|
|
## What's Next?
|
||
|
|
|
||
|
|
- **[Hook Reference](#)** — Detailed documentation of all hooks
|
||
|
|
- **[Example Plugins](#)** — More real-world plugin examples
|
||
|
|
- **[Contributing](#)** — Share your plugins with the community
|