Plugin System
Plugin System
Understanding Plugins
What are Plugins?
Plugins are self-contained extensions that:
- Extend Functionality - Add new features to your forum
- Modify Behavior - Change how things work
- Integrate Services - Connect with external APIs
- Customize Experience - Tailor forum to your needs
Plugin Architecture
Plugins follow a structured architecture:
plugins/
âââ my-plugin/
âââ plugin.json # Plugin metadata
âââ MyPlugin.php # Main plugin class
âââ assets/ # CSS, JS, images
â âââ css/
â âââ js/
â âââ img/
âââ views/ # Template files
â âââ admin.php
âââ README.md # DocumentationInstalling Plugins
Method 1: From the Admin Panel (Recommended, since 5.2.2)
.zip archive â no FTP or shell access required.- Download the plugin
.zipfrom the Flatboard Resource Center or from the plugin author - Go to Admin â Plugins
- Click "Install a plugin"
- Select the
.ziparchive and confirm â the archive is validated, extracted, and the plugin appears in the list automatically
The server validates the archive integrity, MIME type, size, and the presence of a valid plugin.json before extracting.
Method 2: Manual Installation (FTP / SSH)
Plugin packages are distributed as .zip archives. The manual workflow is: download â extract locally â upload the folder â activate.
Step 1: Download and Extract
- Download the plugin
.zipfrom the Flatboard Resource Center or from the plugin author - Verify that the plugin is compatible with your Flatboard 5 version
- Extract the archive on your local machine â you should get a folder (e.g.
my-plugin/) withplugin.jsonat its root
.zip file itself to the server. FTP transfers files individually â you must extract the archive first, then upload the resulting folder.Step 2: Upload the Plugin Folder
Via FTP
Connect to your server with an FTP client (FileZilla, Cyberduck, etc.) and upload the extracted plugin folder into the plugins/ directory of your Flatboard installation:
your-flatboard/
âââ plugins/
âââ my-plugin/ â upload this folder (not the .zip)
âââ plugin.json
âââ ...Via SSH
If you have shell access, you can extract the archive directly on the server:
cd /path/to/your-flatboard/plugins/
unzip my-plugin.zip # extracts to my-plugin/
chmod 755 my-plugin/ -R # adjust permissions if neededStep 3: Activate the Plugin
Once the plugin folder is in place:
- Go to Admin â Plugins
- Find the plugin in the list â it will appear as inactive
- Click Activate
Step 4: Configure (if applicable)
If the plugin has settings, click the settings icon next to the plugin name, fill in the options, and save.
Plugins from Flatboard 3 and 4 are NOT compatible with Flatboard 5 due to the complete architecture rewrite. You'll need to:
- Wait for plugin developers to release Flatboard 5 versions
- Contact plugin developers for updates
- Consider alternative plugins if updates aren't available
Creating a Plugin
Step 1: Create Plugin Structure
mkdir -p plugins/my-plugin/{assets/{css,js,img},views}Step 2: Create plugin.json
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"description": "A custom plugin for Flatboard 5",
"author": "Your Name",
"license": "GPL3",
"requires": {
"flatboard": ">=5.0.0"
},
"class": "App\\Plugins\\MyPlugin\\MyPluginPlugin"
}Step 3: Create Plugin Class
plugins/my-plugin/MyPluginPlugin.php:
<?php
namespace App\Plugins\MyPlugin;
use App\Core\Plugin;
class MyPluginPlugin
{
public function boot()
{
// Register hooks
Plugin::hook('view.header.content', [$this, 'addHeaderContent']);
Plugin::hook('app.routes.register', [$this, 'registerRoutes']);
}
public function addHeaderContent(&$content)
{
$content .= '<link rel="stylesheet" href="proxy.php?url=/plugins/my-plugin/assets/css/style.css">';
}
public function registerRoutes($router)
{
$router->get('/my-plugin', function() {
return 'Hello from my plugin!';
});
}
}Step 4: Register Hooks
Flatboard 5 provides many hooks:
app.routes.register- Register custom routesview.header.styles- Add CSS styles to headerview.footer.content- Add content to footerview.post.avatar- Override post avatar renderingview.user.avatar- Override user avatar rendering anywhere on the forumdiscussion.created- When discussion is createdpost.created- When post is createduser.registered- When user registersuser.updated- When user is updatednotification.types.register- Register custom notification typesvisitor.page_info- Provide page metadata for presence indicator
See the complete hook list below for all available hooks.
Plugin Hooks
Available Hooks
Complete reference of all 74 hooks available in Flatboard 5. All data arguments marked by ref are passed by PHP reference â modifying them changes the actual value used by the framework.
Application
| Hook | Trigger point | Data |
|---|---|---|
app.routes.register | Application bootstrap | Router instance |
router.plugins.register | Router dispatch | Router instance |
security.csp.directives | CSP header generation | CSP directives array by ref |
View â Layout
| Hook | Trigger point | Data |
|---|---|---|
view.header.styles | <head> (frontend + backend) | Styles array by ref |
view.footer.scripts | </body> (frontend + backend) | Scripts array by ref |
view.footer.content | Footer (frontend) | HTML content array by ref |
view.footer.links | Footer links (frontend) | Links array by ref |
view.navbar.items | Main navigation bar | Nav items array by ref |
view.main.before | Before main content (frontend) | HTML string by ref |
view.main.after | After main content (frontend) | HTML string by ref |
view.admin.sidebar.items | Admin sidebar | Sidebar items array by ref |
view.admin.main.before | Before admin main content | HTML string by ref |
view.admin.main.after | After admin main content | HTML string by ref |
View â Banner
| Hook | Trigger point | Data |
|---|---|---|
view.banner.data | Banner component render | Banner data array by ref |
view.banner.content | Banner component render | HTML string by ref, banner data |
View â SEO
| Hook | Trigger point | Data |
|---|---|---|
seo.meta.before | <head> meta tags | SEO data array by ref |
seo.meta.after | After default meta tags | Additional meta HTML string by ref |
View â Auth Forms
| Hook | Trigger point | Data |
|---|---|---|
view.login.form | Login page + login modal | Form fields array by ref |
view.login.validation | Login form submission | Validation error by ref |
view.user.register.form | Registration page + modal | Form fields array by ref |
view.user.register.validation | Registration form submission | Validation error by ref |
View â Discussions & Replies
| Hook | Trigger point | Data |
|---|---|---|
view.discussion.create.form | New discussion form | Form fields array by ref |
view.discussion.create.validation | Discussion creation submission | Validation error by ref |
view.discussion.show.before_content | Discussion view (before first post) | Event data array by ref |
view.discussion.show.after_content | Discussion view (after first post) | Event data array by ref |
view.reply.create.form | Reply form | Form fields array by ref |
view.reply.create.validation | Reply submission | Validation error by ref |
view.sidebar.navigation | Discussion list sidebar | Sidebar items array by ref |
view.post.avatar | Post thread component â avatar render | ['user_id', 'username', 'avatar', 'size', 'html'] by ref â set html to override the default <img> output |
view.user.avatar | components/avatar.php and discussion-item list (author + last-post author) | ['user_id', 'username', 'avatar', 'size', 'html'] by ref â set html to override the rendered avatar for any user across the forum |
Content Events
| Hook | Trigger point | Data |
|---|---|---|
discussion.created | After discussion saved | Enriched event data array |
discussion.created.legacy | After discussion saved | Created discussion (legacy format) |
discussion.deleted | After discussion deleted | Discussion data |
best_answer.set | Best answer selected | ['discussion_id', 'post_id', 'user_id'] |
post.created | After reply saved | Created post data |
post.updated | After reply edited | Updated post data |
post.deleted | After reply deleted | Post data |
category.created | After category saved | Created category data |
category.updated | After category updated | Updated category data |
category.deleted | After category deleted | Category data |
tag.created | After tag saved | Tag data |
User Events
| Hook | Trigger point | Data |
|---|---|---|
user.registered | After registration | Created user data |
user.updated | After user profile saved | Updated user data |
user.deleted | After user account deleted | User data |
ban.created | After ban applied | Updated user data |
ban.removed | After ban lifted | Updated user data |
user.profile.tabs | Profile page render | Tabs array by ref, user array, isOwnProfile bool |
user.profile.tab.contents | Profile page render | Tab contents array by ref, user array, isOwnProfile bool |
user.settings.notifications | Notifications settings form | Options array by ref, preferences array |
user.settings.notifications.save | Notifications settings save | Preferences array by ref, request data, user |
user.settings.preferences.save | General preferences save | Preferences array by ref, request data, user |
Moderation
| Hook | Trigger point | Data |
|---|---|---|
report.created | After content report saved | Created report data |
Search
| Hook | Trigger point | Data |
|---|---|---|
search.results | Search service (all types) | Results array by ref, query, type, keywords, filters |
search.before.display | Before HTML search results | Results array by ref, query, type, filters |
search.ajax.results | Before AJAX search results | Results array by ref, query, type, filters |
Notifications
| Hook | Trigger point | Data |
|---|---|---|
notification.types.register | NotificationService::validateNotificationType() â called at boot time | $allowedTypes array by ref â append custom type strings to extend the core whitelist |
notification.before.create | Before notification written | Notification data array by ref, type string |
notification.after.create | After notification written | Notification ID, notification data |
notification.email.body | Before notification email sent | Email body string by ref, notification data |
Markdown & Editor
| Hook | Trigger point | Data |
|---|---|---|
markdown.parse | Markdown rendering (all contexts) | Result array by ref, raw markdown string |
markdown.editor.config | Editor component init | Config array by ref, $editorId |
component.markdown-editor.before | Before editor textarea | HTML string by ref, $editorId |
component.markdown-editor.attributes | Editor textarea tag | Attributes array by ref, $editorId, config |
markdown.editor.init | Editor JS init script | Script string by ref, $editorId, config |
markdown.editor.getValue | Editor getValue script | Script string by ref, $editorId |
markdown.editor.setValue | Editor setValue script | Script string by ref, $editorId, value path |
component.markdown-editor.after | After editor textarea | HTML string by ref, $editorId |
Presence & Visitor Tracking
| Hook | Trigger point | Data |
|---|---|---|
visitor.page_info | Visitor::getPageInfo() â unknown URLs only | ['page' => string, 'info' => array] by ref â set info['type'], info['title'], info['url'] |
visitor.before_track | VisitorTrackingMiddleware before write | ['ip_address', 'user_agent', 'page', 'user_id'] by ref |
presence.anonymous_visitors | PresenceService::getActiveAnonymousVisitorsDetailed() | Visitors array by ref |
presence.bots | PresenceService::getActiveBotsDetailed() | Bots array by ref |
presence.users | PresenceService::getActiveUsersDetailed() | Users array by ref |
Admin
| Hook | Trigger point | Data |
|---|---|---|
admin.dashboard.widgets | Admin dashboard render | Widgets array by ref |
Plugin Views
| Hook | Trigger point | Data |
|---|---|---|
plugin.view.index | Plugin index view rendered | ['pluginId', 'viewName', 'viewVars'] by ref |
plugin.view.{viewName} | Named plugin view rendered | ['pluginId', 'viewName', 'viewVars'] by ref |
plugin.view.index.permissions | Before plugin index view | Permissions array by ref, pluginId, 'index' |
plugin.view.{viewName}.permissions | Before named plugin view | Permissions array by ref, pluginId, viewName |
plugin.view.{viewName}.vars | Before named plugin view | ViewVars array by ref, pluginId, viewName |
plugin.view.admin.vars | Before plugin admin view | ViewVars array by ref, pluginId, 'admin' |
plugin.view.privacy.vars | Before plugin privacy view | ViewVars array by ref, pluginId, 'privacy' |
plugin.view.cookies.vars | Before plugin cookies view | ViewVars array by ref, pluginId, 'cookies' |
Webhooks
| Hook | Trigger point | Data |
|---|---|---|
webhook.received | Inbound webhook payload | Webhook data array |
Using Hooks
// Register a hook
Plugin::hook('discussion.created', function($discussion) {
// Do something when discussion is created
Logger::info("New discussion: " . $discussion['title']);
});
// Register with priority
Plugin::hook('post.created', [$this, 'handlePostCreated'], 10);
// Extend the notification type whitelist (by reference)
Plugin::hook('notification.types.register', function(array &$types): void {
$types[] = 'my_custom_type';
$types[] = 'reputation_badge';
});
// Override avatar rendering for a virtual user (by reference)
Plugin::hook('view.user.avatar', function(array &$data): void {
if ($data['user_id'] === 'my-bot') {
$data['html'] = '<span class="avatar-bot"><i class="fas fa-robot"></i></span>';
}
});Plugin Configuration
Settings Storage
Plugin settings are stored in the "plugin" section of plugin.json and accessed via Plugin::getData(). Do not use Config::get/set for plugin-specific settings.
use App\Core\Plugin;
// Read a setting (with optional default)
$value = Plugin::getData('my-plugin', 'setting', 'default');
// Dot-notation is supported for nested keys
$host = Plugin::getData('my-plugin', 'smtp.host', '');
// Write a setting (in-memory only)
Plugin::setData('my-plugin', 'setting', 'value');
// Persist all settings to plugin.json
Plugin::saveData('my-plugin', ['setting' => 'value', 'another' => 'data']);Admin Interface
Create admin settings page:
- Create view file:
views/admin.php - Register admin route
- Handle form submission
- Save settings to config
Best Practices
Plugin Development
- Follow Structure - Use standard plugin structure
- Namespace Properly - Use proper namespaces
- Document Code - Add comments and documentation
- Handle Errors - Implement error handling
- Test Thoroughly - Test before release
Security
- Validate Input - Always validate user input
- Sanitize Output - Sanitize all output
- Check Permissions - Verify user permissions
- Use CSRF Protection - Protect forms with CSRF tokens
Performance
- Lazy Load - Load assets only when needed
- Cache When Possible - Cache expensive operations
- Minimize Queries - Optimize database/file access
- Use Hooks Efficiently - Don't overload hooks
Managing Plugins
Activating/Deactivating
- Activate - Enable plugin functionality
- Deactivate - Disable without uninstalling
Uninstalling a Plugin
Uninstalling permanently removes a plugin and all its data from your forum.
Using the Admin Panel
- Navigate to Admin â Plugins
- Deactivate the plugin first â the Uninstall button is blocked for active plugins
- Click the Uninstall button (trash icon, red/danger style) next to the plugin
- Confirm in the confirmation dialog that appears
- The server will:
- Run the plugin's
uninstall()hook (if defined), thendeactivate()hook - Remove all plugin permissions
- Clear asset and translation caches
- Remove the plugin's entry from
plugins.enabled - Recursively delete the plugin directory from
plugins/
- Run the plugin's
stockage/ (JSON or SQLite) is not automatically removed â consult the plugin's documentation for manual cleanup steps if needed.Always create a backup before uninstalling a plugin you may want to restore later.
Restrictions
- Core plugins (marked
cantDisable = "1"inplugin.json) cannot be uninstalled â the button is hidden - Active plugins cannot be uninstalled â deactivate the plugin first
Implementing the Uninstall Hook
Plugins can run cleanup logic when uninstalled:
public function uninstall(): void
{
// Clean up plugin-specific data in stockage/, custom tables, etc.
// Called before the plugin directory is deleted
}
public function deactivate(): void
{
// Called when plugin is deactivated (also called during uninstall)
}Updating Plugins
- Backup - Backup your forum first
- Download - Get new plugin version
- Replace Files - Replace plugin files
- Clear Cache - Clear forum cache
- Test - Test plugin functionality
Plugin Dependencies
Some plugins require other plugins:
{
"requires": {
"flatboard": ">=5.0.0",
"plugins": {
"another-plugin": ">=1.0.0"
}
}
}Troubleshooting
Plugin Not Loading
Check:
- Plugin structure is correct
plugin.jsonis valid- Plugin class exists and is correct
- Check error logs
Plugin Conflicts
Solution:
- Deactivate conflicting plugins
- Check hook priorities
- Review plugin code
- Contact plugin developers
Plugin Errors
Solution:
- Check error logs
- Enable debug mode (temporarily)
- Review plugin code
- Check Flatboard 5 compatibility
Resources
- Developer Guide - Advanced development
- API Documentation - API integration
- Theme Guide - Theme integration
- Troubleshooting - Common issues
Last updated: March 10, 2026