Plugins
Create plugins as single PHP files in plugins/ or as subdirectories with a manifest.json and a bootstrap file. Contributions and distributed plugins are accepted under the terms of the CLA.md.
Plugin Conventions
- File-based plugin: a single PHP file in
plugins/ - Folder-based plugin: a subdirectory in
plugins/with amanifest.jsonand a bootstrap PHP file (e.g.,plugins/editbored/) - Folder-based plugins are required when the plugin needs extra assets (CSS, JS, images, lang files)
Plugin Metadata
File-based plugins expose metadata via PHPDoc comments in the bootstrap file:
/**
* Plugin Name: MyPlugin
* Version: 1.0.0
* Author: Developer
* Description: Example plugin
*/
Folder-based plugins use manifest.json:
{
"name": "editbored",
"version": "1.0.0",
"author": "mlzog",
"description": "WYSIWYG Markdown editor",
"bootstrap": "editbored.php"
}
Hook System
Plugins register callbacks via $pluginManager->addHook('event', $callback).
Core Events
| Event | Arguments | When |
|---|---|---|
after_thread |
$threadId |
After a thread is created |
after_post |
$threadId, $postId |
After a reply is posted |
user_registered |
$userId, $username |
After a user registers |
before_render |
— | Before a page is rendered |
frontend_before_render |
— | Before a frontend page is rendered |
admin_before_render |
— | Before an admin page is rendered |
footer_before_render |
— | Before the footer is rendered |
Example
function myplugin_init() {
global $pluginManager;
$pluginManager->addHook('after_post', function($threadId, $postId) {
// react to new posts
});
}
Directory Structure
plugins/
├── hellobored/ # Example folder-based plugin
│ ├── manifest.json # Required: metadata
│ ├── hellobored.php # Required: bootstrap with hellobored_init()
│ ├── assets/
│ │ ├── css/
│ │ │ └── hellobored.css # Example styles
│ │ └── js/
│ │ └── hellobored.js # Example logic
│ ├── upload.php # Optional: custom endpoint
│ ├── lang/
│ │ └── en.php # Optional: translations
│ └── vendor/ # Optional: third-party assets
└── someplugin.php # Example file-based plugin
Shipping as ZIP
To distribute a plugin as a ZIP package:
- Package your plugin so that the plugin folder is at the root of the ZIP
- For file-based plugins, the PHP file should be at the root of the ZIP
- For folder-based plugins, the folder should be at the root of the ZIP
- Upload via Admin Panel → Plugins → Install Plugin
Recommended ZIP layout:
myplugin-1.0.0.zip
├── myplugin/
│ ├── manifest.json
│ ├── myplugin.php
│ └── assets/...
The installer automatically detects a single top-level folder and flattens it.
Managing Plugins
- Enable / Disable: toggle plugin state without deleting files
- Delete: removes the plugin files and clears state from
data/plugins.json - Install: upload a ZIP to add the plugin
- Update: the Update Manager can apply new versions as ZIP packages
Directory Structure
plugins/
└── myplugin/
├── manifest.json
├── myplugin.php
├── api.php
├── assets/
│ ├── css/
│ │ └── myplugin.css
│ └── js/
│ └── myplugin.js
└── lang/
└── en.php
Localization
Plugins can be localized independently from the core. Each plugin gets its own
translation scope, so plugin strings never collide with the core or with
other plugins (e.g. two plugins may both use a title key without conflict).
Folder-based plugins only (file-based plugins cannot carry lang files): place
translation files under lang/ using the language code as filename:
plugins/myplugin/
└── lang/
├── en.php
└── it.php
Each file returns an associative array:
<?php
return [
'bold' => 'Bold',
'italic' => 'Italic',
];
The strings are loaded automatically into the plugin:<name> scope (e.g.
plugin:myplugin) based on the active language, and are available before the
plugin’s init hook runs.
Usage
Use the pt() helper, which is equivalent to t($key, $params, 'plugin:<name>'):
echo pt('myplugin', 'bold'); // -> 'Bold'
echo pt('myplugin', 'hello', ['name' => 'Joe']); // with {name} placeholder
You may also call the core translation function directly with an explicit scope:
echo t('bold', [], 'plugin:myplugin');
If a key is missing in the plugin’s language file, the key itself is returned
(untranslated) — so a plugin that ships no lang/ directory still works
unchanged. The core translation function t($key, $params) continues to resolve
only from the core scope and is unaffected by plugin translations.
Plugin Manager API
// Discovery
$pluginManager->discover();
$pluginManager->getAll();
$pluginManager->getEnabled();
$pluginManager->getByName('myplugin');
// State
$pluginManager->isEnabled('myplugin');
$pluginManager->enable('myplugin');
$pluginManager->disable('myplugin');
// Localization
$pluginManager->loadTranslations($lang); // loads plugin/lang/{$lang}.php into the plugin:<name> scope
// Lifecycle
$pluginManager->loadEnabled();
$pluginManager->installFromZip('/path/to/plugin.zip');
$pluginManager->delete('myplugin');
// Versioning
$pluginManager->getVersion('myplugin');
// Hooks
$pluginManager->addHook('after_thread', $callback);
$pluginManager->removeHook('after_thread', $callback);
$pluginManager->runHook('after_thread', $threadId);